mirror of
https://github.com/gryf/uc1541.git
synced 2026-02-02 11:55:44 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad6ee3879a | |||
| 76daa17640 | |||
| 3fa965de85 | |||
| f8da7bf58e | |||
| eb57773166 | |||
| 6236015e22 | |||
| 6a455c6c55 |
17
README.rst
17
README.rst
@@ -11,16 +11,16 @@ Features
|
||||
========
|
||||
|
||||
* view, remove, copy files in and out of the image
|
||||
* support for read from and write to d64 images
|
||||
* support for read from and write to d64, d71 and d81 images
|
||||
* gzipped disk images support
|
||||
* onfly characters remapping (see below)
|
||||
|
||||
Remarks
|
||||
=======
|
||||
|
||||
Due to different way of representing file entries on regular D64 disk images,
|
||||
there could be issues with filenames that are transfered from/to the image.
|
||||
Following rules was applied to represent a single file entry:
|
||||
Due to different way of representing file entries on regular Commodore disk
|
||||
images, there could be issues with filenames that are transfered from/to the
|
||||
image. Following rules was applied to represent a single file entry:
|
||||
|
||||
1. An extension is attached to the end of a filename depending on a file type.
|
||||
Possible extensions are: ``prg``, ``del``, ``seq``, ``usr`` and ``rel``.
|
||||
@@ -30,7 +30,7 @@ Following rules was applied to represent a single file entry:
|
||||
3. Every slash character (``/``) will be replaced by pipe character (``|``).
|
||||
4. Leading space will be replaced by tilda (``~``).
|
||||
|
||||
While copying from D64 image to filesystem, filenames will be stored as they
|
||||
While copying from disk image to filesystem, filenames will be stored as they
|
||||
are seen on a listing.
|
||||
|
||||
While copying from filesystem to D64 image, filename conversion will be done:
|
||||
@@ -43,8 +43,8 @@ While copying from filesystem to D64 image, filename conversion will be done:
|
||||
|
||||
Representation of a directory can be sometimes confusing - in case when one
|
||||
copied file without extension it stays there in such form, till next access
|
||||
(after flushing VFS). Also file sizes are not accurate, since D64 directory
|
||||
entries have sizes stored as 256 bytes blocks.
|
||||
(after flushing VFS). Also file sizes are not accurate, since directory entries
|
||||
in CBM format have sizes stored as 256 bytes blocks.
|
||||
|
||||
Screens below shows how it looks like on real C64 machine and its representation
|
||||
on Midnight Commander listing.
|
||||
@@ -67,7 +67,7 @@ Installation
|
||||
* add or change entry for files handle in ``~/.config/mc/mc.ext``::
|
||||
|
||||
# Disk images for Commodore computers (VIC20, C64, C128)
|
||||
regex/\.(d64|D64|d6z|D6z|d6Z|D6Z)$
|
||||
regex/\.([dD]64|[dD]6[zZ]|[dD]71|[dD]7[zZ]|[dD]81|[dD]8[zZ])$
|
||||
Open=%cd %p/uc1541://
|
||||
View=%view{ascii} c1541 %f -list
|
||||
Extract=c1541 %f -extract
|
||||
@@ -87,6 +87,7 @@ script behaviour:
|
||||
Changelog
|
||||
=========
|
||||
|
||||
* **3.3** Added support for .d71 and .d81 disk images.
|
||||
* **3.2** Changed shebang to ``python`` executable instead of ``python3``
|
||||
* **3.1** Argparse on Python3 have different behaviour, where if no subcommand
|
||||
is provided, it will pass anyway. Fixed.
|
||||
|
||||
259
uc1541
259
uc1541
@@ -3,8 +3,8 @@
|
||||
UC1541 Virtual filesystem
|
||||
|
||||
Author: Roman 'gryf' Dobosz <gryf73@gmail.com>
|
||||
Date: 2019-09-15
|
||||
Version: 3.2
|
||||
Date: 2019-09-20
|
||||
Version: 3.3
|
||||
Licence: BSD
|
||||
source: https://bitbucket.org/gryf/uc1541
|
||||
mirror: https://github.com/gryf/uc1541
|
||||
@@ -52,6 +52,9 @@ else:
|
||||
pass
|
||||
|
||||
|
||||
SECLEN = 256
|
||||
|
||||
|
||||
def _ord(string_or_int):
|
||||
"""
|
||||
Return an int value for the (possible) string passed in argument. This
|
||||
@@ -64,9 +67,50 @@ def _ord(string_or_int):
|
||||
return string_or_int
|
||||
|
||||
|
||||
class D64(object):
|
||||
def _get_raw(dimage):
|
||||
"""
|
||||
Implement d64 directory reader
|
||||
Try to get contents of the D64 image either it's gzip compressed or not.
|
||||
"""
|
||||
raw = None
|
||||
with gzip.open(dimage, 'rb') as fobj:
|
||||
# Although the common approach with gzipped files is to check the
|
||||
# magic number, in this case there is no guarantee that first track
|
||||
# does not contain exactly the same byte sequence as the magic number.
|
||||
# So the only way left is to actually try to uncompress the file.
|
||||
try:
|
||||
raw = fobj.read()
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
if not raw:
|
||||
with open(dimage, 'rb') as fobj:
|
||||
raw = fobj.read()
|
||||
|
||||
return raw
|
||||
|
||||
|
||||
def _get_implementation(disk):
|
||||
"""
|
||||
Check the file under fname and return right class for creating an object
|
||||
corresponding for the file
|
||||
"""
|
||||
len_map = {822400: D81, # 80 tracks
|
||||
819200: D81, # 80 tracks, 3200 error bytes
|
||||
349696: D71, # 70 tracks
|
||||
351062: D71, # 70 tracks, 1366 error bytes
|
||||
174848: D64, # usual d64 disc image, 35 tracks, no errors
|
||||
175531: D64, # 35 track, 683 error bytes
|
||||
196608: D64, # 40 track, no errors
|
||||
197376: D64} # 40 track, 768 error bytes
|
||||
|
||||
if disk[:32].startswith(b'C64'):
|
||||
return # T64
|
||||
|
||||
return len_map.get(len(disk))(disk)
|
||||
|
||||
|
||||
class Disk(object):
|
||||
"""
|
||||
Represent common disk interface
|
||||
"""
|
||||
CHAR_MAP = {32: ' ', 33: '!', 34: '"', 35: '#', 37: '%', 38: '&', 39: "'",
|
||||
40: '(', 41: ')', 42: '*', 43: '+', 44: ',', 45: '-', 46: '.',
|
||||
@@ -93,36 +137,20 @@ class D64(object):
|
||||
0b011: 'usr',
|
||||
0b100: 'rel'}
|
||||
|
||||
def __init__(self, dimage):
|
||||
DIR_TRACK = 18
|
||||
DIR_SECTOR = 1
|
||||
|
||||
def __init__(self, raw):
|
||||
"""
|
||||
Init
|
||||
"""
|
||||
LOG.debug('image: %s', dimage)
|
||||
self.raw = None
|
||||
self.raw = raw
|
||||
self.current_sector_data = None
|
||||
self.next_sector = 0
|
||||
self.next_track = None
|
||||
self._dir_contents = []
|
||||
self._already_done = []
|
||||
|
||||
self._get_raw(dimage)
|
||||
|
||||
def _get_raw(self, dimage):
|
||||
"""Try to get contents of the D64 image either it's gzip compressed or
|
||||
not."""
|
||||
fobj = gzip.open(dimage, 'rb')
|
||||
# Although the common approach with gzipped files is to check the
|
||||
# magic number, in this case there is no guarantee that first track
|
||||
# does not contain exactly the same byte sequence as the magic number.
|
||||
# So the only way left is to actually try to uncompress the file.
|
||||
try:
|
||||
self.raw = fobj.read()
|
||||
except (IOError, OSError):
|
||||
fobj.close()
|
||||
fobj = open(dimage, 'rb')
|
||||
self.raw = fobj.read()
|
||||
fobj.close()
|
||||
|
||||
def _map_filename(self, string):
|
||||
"""
|
||||
Transcode filename to ASCII compatible. Replace not supported
|
||||
@@ -161,17 +189,18 @@ class D64(object):
|
||||
return False
|
||||
|
||||
if self.next_track is None:
|
||||
LOG.debug("Going to the track: 18,1")
|
||||
offset = self._get_d64_offset(18, 1)
|
||||
LOG.debug("Going to the track: %s, %s", self.DIR_TRACK,
|
||||
self.DIR_SECTOR)
|
||||
offset = self._get_offset(self.DIR_TRACK, self.DIR_SECTOR)
|
||||
else:
|
||||
offset = self._get_d64_offset(self.next_track, self.next_sector)
|
||||
offset = self._get_offset(self.next_track, self.next_sector)
|
||||
LOG.debug("Going to the track: %s,%s", self.next_track,
|
||||
self.next_sector)
|
||||
|
||||
self.current_sector_data = self.raw[offset:offset + 256]
|
||||
self.current_sector_data = self.raw[offset:offset + SECLEN]
|
||||
|
||||
# Guard for reading data out of bound - that happened for discs which
|
||||
# store only raw data, even on 18 track
|
||||
# store only raw data, even on directory track
|
||||
if not self.current_sector_data:
|
||||
return False
|
||||
|
||||
@@ -197,30 +226,11 @@ class D64(object):
|
||||
num & 2 and 1,
|
||||
num & 1), 2), '???')
|
||||
|
||||
def _get_d64_offset(self, track, sector):
|
||||
def _get_offset(self, track, sector):
|
||||
"""
|
||||
Return offset (in bytes) for specified track and sector.
|
||||
"""
|
||||
|
||||
offset = 0
|
||||
truncate_track = 0
|
||||
|
||||
if track > 17:
|
||||
offset = 17 * 21 * 256
|
||||
truncate_track = 17
|
||||
|
||||
if track > 24:
|
||||
offset += 6 * 19 * 256
|
||||
truncate_track = 24
|
||||
|
||||
if track > 30:
|
||||
offset += 5 * 18 * 256
|
||||
truncate_track = 30
|
||||
|
||||
track = track - truncate_track
|
||||
offset += track * sector * 256
|
||||
|
||||
return offset
|
||||
return 0
|
||||
|
||||
def _harvest_entries(self):
|
||||
"""
|
||||
@@ -261,6 +271,123 @@ class D64(object):
|
||||
return self._dir_contents
|
||||
|
||||
|
||||
class D64(Disk):
|
||||
"""
|
||||
Implement d64 directory reader
|
||||
"""
|
||||
|
||||
def _get_offset(self, track, sector):
|
||||
"""
|
||||
Return offset (in bytes) for specified track and sector.
|
||||
|
||||
Track Sectors/track # Tracks
|
||||
----- ------------- ---------
|
||||
1-17 21 17
|
||||
18-24 19 7
|
||||
25-30 18 6
|
||||
31-40 17 10
|
||||
"""
|
||||
offset = 0
|
||||
truncate_track = 0
|
||||
|
||||
if track > 17:
|
||||
offset = 17 * 21 * SECLEN
|
||||
truncate_track = 17
|
||||
|
||||
if track > 24:
|
||||
offset += 7 * 19 * SECLEN
|
||||
truncate_track = 24
|
||||
|
||||
if track > 30:
|
||||
offset += 6 * 18 * SECLEN
|
||||
truncate_track = 30
|
||||
|
||||
track = track - truncate_track
|
||||
offset += track * sector * SECLEN
|
||||
|
||||
return offset
|
||||
|
||||
|
||||
class D71(Disk):
|
||||
"""
|
||||
Implement d71 directory reader
|
||||
"""
|
||||
|
||||
def _get_offset(self, track, sector):
|
||||
"""
|
||||
Return offset (in bytes) for specified track and sector.
|
||||
|
||||
Track Sec/trk # Tracks
|
||||
-------------- ------- ---------
|
||||
1-17 (side 0) 21 17
|
||||
18-24 (side 0) 19 7
|
||||
25-30 (side 0) 18 6
|
||||
31-35 (side 0) 17 5
|
||||
36-52 (side 1) 21 17
|
||||
53-59 (side 1) 19 7
|
||||
60-65 (side 1) 18 6
|
||||
66-70 (side 1) 17 5
|
||||
"""
|
||||
offset = 0
|
||||
truncate_track = 0
|
||||
|
||||
if track > 17:
|
||||
offset = 17 * 21 * SECLEN
|
||||
truncate_track = 17
|
||||
|
||||
if track > 24:
|
||||
offset += 7 * 19 * SECLEN
|
||||
truncate_track = 24
|
||||
|
||||
if track > 30:
|
||||
offset += 6 * 18 * SECLEN
|
||||
truncate_track = 30
|
||||
|
||||
if track > 35:
|
||||
offset += 5 * 17 * SECLEN
|
||||
truncate_track = 35
|
||||
|
||||
if track > 52:
|
||||
offset = 17 * 21 * SECLEN
|
||||
truncate_track = 17
|
||||
|
||||
if track > 59:
|
||||
offset += 7 * 19 * SECLEN
|
||||
truncate_track = 24
|
||||
|
||||
if track > 65:
|
||||
offset += 6 * 18 * SECLEN
|
||||
truncate_track = 30
|
||||
|
||||
track = track - truncate_track
|
||||
offset += track * sector * SECLEN
|
||||
|
||||
return offset
|
||||
|
||||
|
||||
class D81(Disk):
|
||||
"""
|
||||
Implement d81 directory reader
|
||||
"""
|
||||
DIR_TRACK = 40
|
||||
DIR_SECTOR = 3
|
||||
FILE_TYPES = {0b000: 'del',
|
||||
0b001: 'seq',
|
||||
0b010: 'prg',
|
||||
0b011: 'usr',
|
||||
0b100: 'rel',
|
||||
0b101: 'cbm'}
|
||||
|
||||
def _get_offset(self, track, sector):
|
||||
"""
|
||||
Return offset (in bytes) for specified track and sector. In d81 is
|
||||
easy, since we have 80 tracks with 40 sectors for 256 bytes each.
|
||||
"""
|
||||
# we wan to go to the beginning (first sector) of the track, not it's
|
||||
# max, so that we need to extract its amount.
|
||||
return (track * 40 - 40) * SECLEN + sector * SECLEN
|
||||
|
||||
|
||||
class Uc1541(object):
|
||||
"""
|
||||
Class for interact with c1541 program and MC
|
||||
@@ -274,7 +401,7 @@ class Uc1541(object):
|
||||
self._verbose = os.getenv("UC1541_VERBOSE", False)
|
||||
self._hide_del = os.getenv("UC1541_HIDE_DEL", False)
|
||||
|
||||
self.pyd64 = D64(archname).list_dir()
|
||||
self.dirlist = _get_implementation(_get_raw(archname)).list_dir()
|
||||
self.file_map = {}
|
||||
self.directory = []
|
||||
|
||||
@@ -307,14 +434,6 @@ class Uc1541(object):
|
||||
if not self._call_command('delete', dst=dst):
|
||||
return self._show_error()
|
||||
|
||||
# During removing, a message containing ERRORCODE is sent to stdout
|
||||
# instead of stderr. Everything other than 'ERRORCODE 1' (which means:
|
||||
# 'everything fine') is actually a failure. In case of verbose error
|
||||
# output it is needed to copy self.out to self.err.
|
||||
if '\nERRORCODE 1\n' not in self.out:
|
||||
self.err = self.out
|
||||
return self._show_error()
|
||||
|
||||
return 0
|
||||
|
||||
def copyin(self, dst, src):
|
||||
@@ -416,7 +535,7 @@ class Uc1541(object):
|
||||
continue
|
||||
|
||||
display_name = ".".join([fname, ext])
|
||||
pattern_name = self.pyd64[idx]['fname']
|
||||
pattern_name = self.dirlist[idx]['fname']
|
||||
|
||||
if '/' in display_name:
|
||||
display_name = display_name.replace('/', '|')
|
||||
@@ -438,7 +557,7 @@ class Uc1541(object):
|
||||
'display_name': display_name,
|
||||
'uid': uid,
|
||||
'gid': gid,
|
||||
'size': int(blocks) * 256,
|
||||
'size': int(blocks) * SECLEN,
|
||||
'perms': perms})
|
||||
idx += 1
|
||||
return directory
|
||||
@@ -467,14 +586,19 @@ class Uc1541(object):
|
||||
command.append(dst)
|
||||
|
||||
LOG.debug('executing command: %s', ' '.join(command))
|
||||
# For some reason using write command and reading output confuses
|
||||
# Python3 beneath MC and as a consequence MC report an error...
|
||||
# therefore for write command let's not use universal_newlines...
|
||||
# For some reason using write and delete commands and reading output
|
||||
# confuses Python3 beneath MC and as a consequence MC report an
|
||||
# error...therefore for those commands let's not use
|
||||
# universal_newlines...
|
||||
universal_newlines = True
|
||||
if cmd in ['delete', 'write']:
|
||||
universal_newlines = False
|
||||
self.out, self.err = Popen(command,
|
||||
universal_newlines=(cmd != 'write'),
|
||||
universal_newlines=universal_newlines,
|
||||
stdout=PIPE, stderr=PIPE).communicate()
|
||||
|
||||
LOG.debug('an err: %s', self.err)
|
||||
if self.err:
|
||||
LOG.debug('an err: %s', self.err)
|
||||
return not self.err
|
||||
|
||||
|
||||
@@ -566,6 +690,7 @@ def no_parse():
|
||||
|
||||
return CALL_MAP[sys.argv[1]](arg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
LOG.debug("Script params: %s", str(sys.argv))
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user