mirror of
https://github.com/gryf/uc1541.git
synced 2026-02-02 11:55:44 +01:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f359f7191 | |||
| 60dfd1f687 | |||
| ad6ee3879a | |||
| 76daa17640 | |||
| 3fa965de85 | |||
| f8da7bf58e | |||
| eb57773166 | |||
| 6236015e22 | |||
| 6a455c6c55 | |||
| 45bde06dd3 | |||
| 03b6236463 | |||
| 711695be2b | |||
| 197ba7a707 | |||
| 58d3fce340 | |||
| d4e3794c1c | |||
| 7c2e6f19a9 | |||
| fa81fc0b57 | |||
| 3822ad2531 | |||
| 909a42dd28 | |||
| d44c9e28fa | |||
| 0bdb6d8694 | |||
| 1346a2fcf8 |
24
LICENSE
Normal file
24
LICENSE
Normal file
@@ -0,0 +1,24 @@
|
||||
Copyright (c) 2015, Roman Dobosz
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the organization nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL ROMAN DOBOSZ BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
130
README.rst
Normal file
130
README.rst
Normal file
@@ -0,0 +1,130 @@
|
||||
===================================================
|
||||
Midnight Commander virtual filesystem for C64 disks
|
||||
===================================================
|
||||
|
||||
This extfs provides an access to disk image files for the Commodore
|
||||
VIC20/C64/C128 for Midnight Commander. It requires the utility c1541 that comes
|
||||
bundled with Vice, the emulator for the VIC20, C64, C128 and other computers
|
||||
made by Commodore.
|
||||
|
||||
Features
|
||||
========
|
||||
|
||||
* View, remove, copy files in and out of the image,
|
||||
* Support for read from and write to d64, d71 and d81 images,
|
||||
* Gzipped disk images support,
|
||||
* On fly characters remapping (see below).
|
||||
|
||||
Remarks
|
||||
=======
|
||||
|
||||
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``.
|
||||
2. Every non-ASCII character (which could be some of characters specific to
|
||||
PET-ASCII, or be a control character) will be replaced by dot (``.``), since
|
||||
``c1541`` program will list them that way.
|
||||
3. Every slash character (``/``) will be replaced by pipe character (``|``).
|
||||
4. Leading space will be replaced by tilda (``~``).
|
||||
|
||||
While copying from disk image to filesystem, filenames will be stored as they
|
||||
are seen on a listing.
|
||||
|
||||
While copying from filesystem to disk image, filename conversion will be done:
|
||||
|
||||
1. Every ``$`` and ``*`` characters will be replaced by question mark (``?``)
|
||||
2. Every pipe (``|``) and backslash (``\``) characters will be replaced by
|
||||
slash (``/``)
|
||||
3. Every tilde (``~``) will be replaced by a space
|
||||
4. ``prg`` extension will be truncated
|
||||
|
||||
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 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.
|
||||
|
||||
|image1| |image2|
|
||||
|
||||
In ``media`` directory you can find the ``test.d6z`` gzip compressed D64 image
|
||||
file.
|
||||
|
||||
Rquirements
|
||||
===========
|
||||
|
||||
* Python 3.6 or higher (checked recently with 3.11)
|
||||
* Vice installation (c1541 program in path)
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
* copy ``uc1541`` to ``~/.local/share/mc/extfs.d/``
|
||||
* add or change entry for files handle in ``~/.config/mc/mc.ext``::
|
||||
|
||||
# Disk images for Commodore computers (VIC20, C64, C128)
|
||||
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
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Here are specific for this script variable, which while set, can influence
|
||||
script behaviour:
|
||||
|
||||
* ``UC1541_DEBUG`` - if set, uc1541 will produce log in /tmp/uc1541.log file.
|
||||
* ``UC1541_VERBOSE`` - if set, script will be more verbose, i.e. error messages
|
||||
form c1541 program will be passed to Midnight Commander, so that user will be
|
||||
aware of error cause if any.
|
||||
* ``UC1541_HIDE_DEL`` - if set, no DEL entries will be shown.
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
* **3.5** Drop Python2 support.
|
||||
* **3.4** Code cleanup. Removed dummy logger class and sys.args based argument
|
||||
parsing.
|
||||
* **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.
|
||||
* **3.0** Added beta quality Python3 support
|
||||
* **2.8** Treat non standard discs a bit better
|
||||
* **2.7** Added support for gzipped disk images
|
||||
* **2.6** Added mkdir and run handling (or rather lack of handling :). Minor
|
||||
refactoring.
|
||||
* **2.5** Fixed bug with filenames started with a '-' sign.
|
||||
* **2.4** Fixed endless loop bug for reading directory in Python implemented
|
||||
directory reader.
|
||||
* **2.3** Re added and missing method _correct_fname used for writing files
|
||||
into d64 image.
|
||||
* **2.2** Fixed bug(?) with unusual sector end (marked as sector 0, not 255),
|
||||
causing endless directory reading on random locations.
|
||||
* **2.1** Fixed bug with filenames containing slash.
|
||||
* **2.0** Added reading raw D64 image, and mapping for jokers. Now it is
|
||||
possible to read files with PET-ASCII/control sequences in filenames. Working
|
||||
with d64 images only. Added workaround for space at the beginning of the
|
||||
filename.
|
||||
* **1.2** Added configuration env variables: ``UC1541_VERBOSE`` and
|
||||
``UC1541_HIDE_DEL``. First one, if set to any value, will cause that error
|
||||
messages from ``c1541`` program will be redirected as a failure messages
|
||||
visible in MC.
|
||||
The other variable, when set to any value, cause *del* entries to be not
|
||||
shown in the lister.
|
||||
* **1.1** Added protect bits, added failsafe for ``argparse`` module
|
||||
* **1.0** Initial release
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
This software is licensed under 3-clause BSD license. See LICENSE file for
|
||||
details.
|
||||
|
||||
.. |image1| image:: /media/vice_dir.png?raw=true
|
||||
.. |image2| image:: /media/mc.png?raw=true
|
||||
BIN
media/mc.png
Normal file
BIN
media/mc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
media/test.d6z
Normal file
BIN
media/test.d6z
Normal file
Binary file not shown.
BIN
media/vice_dir.png
Normal file
BIN
media/vice_dir.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
526
uc1541
526
uc1541
@@ -1,88 +1,25 @@
|
||||
#! /usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
UC1541 Virtual filesystem
|
||||
|
||||
This extfs provides an access to disk image files for the Commodore
|
||||
VIC20/C64/C128. It requires the utility c1541 that comes bundled with Vice,
|
||||
the emulator for the VIC20, C64, C128 and other computers made by Commodore.
|
||||
|
||||
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:
|
||||
|
||||
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.
|
||||
2. Every non-ASCII character (which could be some of characters specific to
|
||||
PET-ASCII, or be a control character) will be replaced by dot (.), since
|
||||
c1541 program will list them that way.
|
||||
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
|
||||
are seen on a listing.
|
||||
|
||||
While copying from filesystem to D64 image, filename conversion will be done:
|
||||
1. Every $ and * characters will be replaced by question mark (?)
|
||||
2. Every pipe (|) and backslash (\) characters will be replaced by slash (/)
|
||||
3. Every tilda (~) will be replaced by a space
|
||||
4. 'prg' extension will be truncated
|
||||
|
||||
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.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Here are specific for this script variable, which while set, can influence
|
||||
script behaviour:
|
||||
|
||||
UC1541_DEBUG - if set, uc1541 will produce log in /tmp/uc1541.log file
|
||||
|
||||
UC1541_VERBOSE - of set, script will be more verbose, i.e. error messages form
|
||||
c1541 program will be passed to Midnight Commander, so that user will be aware
|
||||
of error cause if any.
|
||||
|
||||
UC1541_HIDE_DEL - if set, no DEL entries will be shown
|
||||
|
||||
Changelog:
|
||||
2.4 Fixed endless loop bug for reading directory in Python implemented
|
||||
directory reader.
|
||||
2.3 Re added and missing method _correct_fname used for writing files
|
||||
into d64 image.
|
||||
2.2 Fixed bug(?) with unusual sector end (marked as sector 0, not 255),
|
||||
causing endless directory reading on random locations.
|
||||
2.1 Fixed bug with filenames containing slash.
|
||||
2.0 Added reading raw D64 image, and mapping for jokers. Now it is
|
||||
possible to read files with PET-ASCII/control sequences in filenames.
|
||||
Working with d64 images only. Added workaround for space at the
|
||||
beggining of the filename.
|
||||
1.2 Added configuration env variables: UC1541_VERBOSE and UC1541_HIDE_DEL.
|
||||
First one, if set to any value, will cause that error messages from
|
||||
c1541 program will be redirected as a failure messages visible in MC.
|
||||
The other variable, when set to any value, cause "del" entries to be
|
||||
not shown in the lister.
|
||||
1.1 Added protect bits, added failsafe for argparse module
|
||||
1.0 Initial release
|
||||
|
||||
Author: Roman 'gryf' Dobosz <gryf73@gmail.com>
|
||||
Date: 2012-09-30
|
||||
Version: 2.4
|
||||
Date: 2023-10-04
|
||||
Version: 3.5
|
||||
Licence: BSD
|
||||
source: https://bitbucket.org/gryf/uc1541
|
||||
mirror: https://github.com/gryf/uc1541
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
import gzip
|
||||
import logging
|
||||
import os
|
||||
from subprocess import Popen, PIPE
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
LOG = logging.getLogger('UC1541')
|
||||
if os.getenv('UC1541_DEBUG'):
|
||||
import logging
|
||||
LOG = logging.getLogger('UC1541')
|
||||
LOG.setLevel(logging.DEBUG)
|
||||
FILE_HANDLER = logging.FileHandler("/tmp/uc1541.log")
|
||||
FILE_FORMATTER = logging.Formatter("%(asctime)s %(levelname)-8s "
|
||||
@@ -90,35 +27,55 @@ if os.getenv('UC1541_DEBUG'):
|
||||
FILE_HANDLER.setFormatter(FILE_FORMATTER)
|
||||
FILE_HANDLER.setLevel(logging.DEBUG)
|
||||
LOG.addHandler(FILE_HANDLER)
|
||||
else:
|
||||
class LOG(object):
|
||||
"""
|
||||
Dummy logger object. does nothing.
|
||||
"""
|
||||
@classmethod
|
||||
def debug(*args, **kwargs):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def info(*args, **kwargs):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def warning(*args, **kwargs):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def error(*args, **kwargs):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def critical(*args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class D64(object):
|
||||
SECLEN = 256
|
||||
|
||||
|
||||
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: '.',
|
||||
@@ -145,15 +102,14 @@ 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)
|
||||
dimage = open(dimage, 'rb')
|
||||
self.raw = dimage.read()
|
||||
dimage.close()
|
||||
|
||||
self.raw = raw
|
||||
self.current_sector_data = None
|
||||
self.next_sector = 0
|
||||
self.next_track = None
|
||||
@@ -169,12 +125,16 @@ class D64(object):
|
||||
filename = list()
|
||||
|
||||
for chr_ in string:
|
||||
if ord(chr_) == 160: # shift+space character; $a0
|
||||
if chr_ == 160: # shift+space character; $a0
|
||||
break
|
||||
|
||||
character = D64.CHAR_MAP.get(ord(chr_), '?')
|
||||
character = D64.CHAR_MAP.get(chr_, '?')
|
||||
filename.append(character)
|
||||
|
||||
# special cases
|
||||
if filename[0] == "-":
|
||||
filename[0] = "?"
|
||||
|
||||
LOG.debug("string: ``%s'' mapped to: ``%s''", string,
|
||||
"".join(filename))
|
||||
return "".join(filename)
|
||||
@@ -194,17 +154,23 @@ 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]
|
||||
|
||||
self.next_track = ord(self.current_sector_data[0])
|
||||
self.next_sector = ord(self.current_sector_data[1])
|
||||
# Guard for reading data out of bound - that happened for discs which
|
||||
# store only raw data, even on directory track
|
||||
if not self.current_sector_data:
|
||||
return False
|
||||
|
||||
self.next_track = self.current_sector_data[0]
|
||||
self.next_sector = self.current_sector_data[1]
|
||||
|
||||
if (self.next_track, self.next_sector) in self._already_done:
|
||||
# Just a failsafe. Endless loop is not what is expected.
|
||||
@@ -225,39 +191,20 @@ 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):
|
||||
"""
|
||||
Traverse through sectors and store entries in _dir_contents
|
||||
"""
|
||||
sector = self.current_sector_data
|
||||
for x in range(8):
|
||||
for dummy in range(8):
|
||||
entry = sector[:32]
|
||||
ftype = ord(entry[2])
|
||||
ftype = entry[2]
|
||||
|
||||
if ftype == 0: # deleted
|
||||
sector = sector[32:]
|
||||
@@ -265,12 +212,12 @@ class D64(object):
|
||||
|
||||
type_verbose = self._get_ftype(ftype)
|
||||
|
||||
protect = ord(entry[2]) & 64 and "<" or " "
|
||||
protect = entry[2] & 64 and "<" or " "
|
||||
fname = entry[5:21]
|
||||
if ftype == 'rel':
|
||||
size = ord(entry[23])
|
||||
size = entry[23]
|
||||
else:
|
||||
size = ord(entry[30]) + ord(entry[31]) * 226
|
||||
size = entry[30] + entry[31] * 226
|
||||
|
||||
self._dir_contents.append({'fname': self._map_filename(fname),
|
||||
'ftype': type_verbose,
|
||||
@@ -289,6 +236,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
|
||||
@@ -302,7 +366,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 = []
|
||||
|
||||
@@ -315,6 +379,11 @@ class Uc1541(object):
|
||||
LOG.info("List contents of %s", self.arch)
|
||||
directory = self._get_dir()
|
||||
|
||||
# If there is an error reading directory, show the reason to the user
|
||||
if self.out.startswith("Error"):
|
||||
sys.stderr.write(self.out.split("\n")[0] + "\n")
|
||||
return 2
|
||||
|
||||
for entry in directory:
|
||||
sys.stdout.write("%(perms)s 1 %(uid)-8d %(gid)-8d %(size)8d "
|
||||
"Jan 01 1980 %(display_name)s\n" % entry)
|
||||
@@ -326,19 +395,7 @@ class Uc1541(object):
|
||||
"""
|
||||
LOG.info("Removing file %s", dst)
|
||||
dst = self._get_masked_fname(dst)
|
||||
|
||||
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
|
||||
return self._call_command('delete', dst=dst)
|
||||
|
||||
def copyin(self, dst, src):
|
||||
"""
|
||||
@@ -346,11 +403,7 @@ class Uc1541(object):
|
||||
"""
|
||||
LOG.info("Copy into D64 %s as %s", src, dst)
|
||||
dst = self._correct_fname(dst)
|
||||
|
||||
if not self._call_command('write', src=src, dst=dst):
|
||||
return self._show_error()
|
||||
|
||||
return 0
|
||||
return self._call_command('write', src=src, dst=dst)
|
||||
|
||||
def copyout(self, src, dst):
|
||||
"""
|
||||
@@ -363,10 +416,17 @@ class Uc1541(object):
|
||||
|
||||
src = self._get_masked_fname(src)
|
||||
|
||||
if not self._call_command('read', src=src, dst=dst):
|
||||
return self._show_error()
|
||||
return self._call_command('read', src=src, dst=dst)
|
||||
|
||||
return 0
|
||||
def mkdir(self, dirname):
|
||||
"""Not supported"""
|
||||
self.err = "D64 format doesn't support directories"
|
||||
return self._show_error()
|
||||
|
||||
def run(self, fname):
|
||||
"""Not supported"""
|
||||
self.err = "Not supported, unless you are using MC on real C64 ;)"
|
||||
return self._show_error()
|
||||
|
||||
def _correct_fname(self, fname):
|
||||
"""
|
||||
@@ -417,8 +477,9 @@ class Uc1541(object):
|
||||
uid = os.getuid()
|
||||
gid = os.getgid()
|
||||
|
||||
if not self._call_command('list'):
|
||||
return self._show_error()
|
||||
res = self._call_command('list')
|
||||
if res != 0:
|
||||
return res
|
||||
|
||||
idx = 0
|
||||
for line in self.out.split("\n"):
|
||||
@@ -429,14 +490,18 @@ 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('/', '|')
|
||||
|
||||
# workaround for space at the beggining of the filename
|
||||
if display_name[0] == ' ':
|
||||
display_name = '~' + display_name[1:]
|
||||
# workaround for space and dash at the beggining of the
|
||||
# filename
|
||||
char_map = {' ': '~',
|
||||
'-': '_'}
|
||||
display_name = "".join([char_map.get(display_name[0],
|
||||
display_name[0]),
|
||||
display_name[1:]])
|
||||
|
||||
if ext == 'del':
|
||||
perms = "----------"
|
||||
@@ -447,7 +512,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
|
||||
@@ -457,9 +522,9 @@ class Uc1541(object):
|
||||
Pass out error output from c1541 execution
|
||||
"""
|
||||
if self._verbose:
|
||||
sys.exit(self.err)
|
||||
return self.err
|
||||
else:
|
||||
sys.exit(1)
|
||||
return 1
|
||||
|
||||
def _call_command(self, cmd, src=None, dst=None):
|
||||
"""
|
||||
@@ -470,29 +535,45 @@ class Uc1541(object):
|
||||
dir/list
|
||||
"""
|
||||
command = ['c1541', '-attach', self.arch, '-%s' % cmd]
|
||||
if src and dst:
|
||||
if src:
|
||||
command.append(src)
|
||||
if dst:
|
||||
command.append(dst)
|
||||
elif src or dst:
|
||||
command.append(src and src or dst)
|
||||
|
||||
self.out, self.err = Popen(command, stdout=PIPE,
|
||||
stderr=PIPE).communicate()
|
||||
return not self.err
|
||||
LOG.debug('executing command: %s', ' '.join(command))
|
||||
# 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) = subprocess.Popen(command,
|
||||
universal_newlines=universal_newlines,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE).communicate()
|
||||
|
||||
if self.err:
|
||||
LOG.debug('an err: %s', self.err)
|
||||
return self._show_error()
|
||||
return 0
|
||||
|
||||
|
||||
CALL_MAP = {'list': lambda a: Uc1541(a.ARCH).list(),
|
||||
'copyin': lambda a: Uc1541(a.ARCH).copyin(a.SRC, a.DST),
|
||||
'copyout': lambda a: Uc1541(a.ARCH).copyout(a.SRC, a.DST),
|
||||
'rm': lambda a: Uc1541(a.ARCH).rm(a.DST)}
|
||||
CALL_MAP = {'list': lambda a: Uc1541(a.arch).list(),
|
||||
'copyin': lambda a: Uc1541(a.arch).copyin(a.src, a.dst),
|
||||
'copyout': lambda a: Uc1541(a.arch).copyout(a.src, a.dst),
|
||||
'mkdir': lambda a: Uc1541(a.arch).mkdir(a.dst),
|
||||
'rm': lambda a: Uc1541(a.arch).rm(a.dst),
|
||||
'run': lambda a: Uc1541(a.arch).run(a.dst)}
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""
|
||||
Use ArgumentParser to check for script arguments and execute.
|
||||
"""
|
||||
parser = ArgumentParser()
|
||||
subparsers = parser.add_subparsers(help='supported commands')
|
||||
"""Use ArgumentParser to check for script arguments and execute."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(help='supported commands',
|
||||
dest='subcommand')
|
||||
subparsers.required = True
|
||||
parser_list = subparsers.add_parser('list', help="List contents of D64 "
|
||||
"image")
|
||||
parser_copyin = subparsers.add_parser('copyin', help="Copy file into D64 "
|
||||
@@ -500,67 +581,42 @@ def parse_args():
|
||||
parser_copyout = subparsers.add_parser('copyout', help="Copy file out of "
|
||||
"D64 image")
|
||||
parser_rm = subparsers.add_parser('rm', help="Delete file from D64 image")
|
||||
parser_mkdir = subparsers.add_parser('mkdir', help="Create directory in "
|
||||
"archive")
|
||||
parser_run = subparsers.add_parser('run', help="Execute archived file")
|
||||
|
||||
parser_list.add_argument('ARCH', help="D64 Image filename")
|
||||
parser_list.add_argument('arch', help="D64 Image filename")
|
||||
parser_list.set_defaults(func=CALL_MAP['list'])
|
||||
|
||||
parser_copyin.add_argument('ARCH', help="D64 Image filename")
|
||||
parser_copyin.add_argument('SRC', help="Source filename")
|
||||
parser_copyin.add_argument('DST', help="Destination filename (to be "
|
||||
parser_copyin.add_argument('arch', help="D64 Image filename")
|
||||
parser_copyin.add_argument('src', help="Source filename")
|
||||
parser_copyin.add_argument('dst', help="Destination filename (to be "
|
||||
"written into D64 image)")
|
||||
parser_copyin.set_defaults(func=CALL_MAP['copyin'])
|
||||
|
||||
parser_copyout.add_argument('ARCH', help="D64 Image filename")
|
||||
parser_copyout.add_argument('SRC', help="Source filename (to be read from"
|
||||
parser_copyout.add_argument('arch', help="D64 Image filename")
|
||||
parser_copyout.add_argument('src', help="Source filename (to be read from"
|
||||
" D64 image")
|
||||
parser_copyout.add_argument('DST', help="Destination filename")
|
||||
parser_copyout.add_argument('dst', help="Destination filename")
|
||||
parser_copyout.set_defaults(func=CALL_MAP['copyout'])
|
||||
|
||||
parser_rm.add_argument('ARCH', help="D64 Image filename")
|
||||
parser_rm.add_argument('DST', help="File inside D64 image to be deleted")
|
||||
parser_rm.add_argument('arch', help="D64 Image filename")
|
||||
parser_rm.add_argument('dst', help="File inside D64 image to be deleted")
|
||||
parser_rm.set_defaults(func=CALL_MAP['rm'])
|
||||
|
||||
parser_mkdir.add_argument('arch', help="archive filename")
|
||||
parser_mkdir.add_argument('dst', help="Directory name inside archive to "
|
||||
"be created")
|
||||
parser_mkdir.set_defaults(func=CALL_MAP['mkdir'])
|
||||
|
||||
parser_run.add_argument('arch', help="archive filename")
|
||||
parser_run.add_argument('dst', help="File to be executed")
|
||||
parser_run.set_defaults(func=CALL_MAP['run'])
|
||||
|
||||
args = parser.parse_args()
|
||||
return args.func(args)
|
||||
|
||||
|
||||
def no_parse():
|
||||
"""
|
||||
Failsafe argument "parsing". Note, that it blindly takes positional
|
||||
arguments without checking them. In case of wrong arguments it will
|
||||
silently exit
|
||||
"""
|
||||
try:
|
||||
if sys.argv[1] not in ('list', 'copyin', 'copyout', 'rm'):
|
||||
sys.exit(2)
|
||||
except IndexError:
|
||||
sys.exit(2)
|
||||
|
||||
class Arg(object):
|
||||
DST = None
|
||||
SRC = None
|
||||
ARCH = None
|
||||
|
||||
arg = Arg()
|
||||
|
||||
try:
|
||||
arg.ARCH = sys.argv[2]
|
||||
if sys.argv[1] in ('copyin', 'copyout'):
|
||||
arg.SRC = sys.argv[3]
|
||||
arg.DST = sys.argv[4]
|
||||
elif sys.argv[1] == 'rm':
|
||||
arg.DST = sys.argv[3]
|
||||
except IndexError:
|
||||
sys.exit(2)
|
||||
|
||||
CALL_MAP[sys.argv[1]](arg)
|
||||
|
||||
if __name__ == "__main__":
|
||||
LOG.debug("Script params: %s", str(sys.argv))
|
||||
try:
|
||||
from argparse import ArgumentParser
|
||||
parse_func = parse_args
|
||||
except ImportError:
|
||||
parse_func = no_parse
|
||||
|
||||
parse_func()
|
||||
sys.exit(parse_args())
|
||||
|
||||
Reference in New Issue
Block a user