1
0
mirror of https://github.com/gryf/uc1541.git synced 2026-02-02 19:55:46 +01:00

25 Commits
v2.0 ... v3.4

Author SHA1 Message Date
60dfd1f687 Code cleanup. 2020-06-28 11:56:00 +02:00
ad6ee3879a Clean up the code.
There was an issue regarding c1541 in old version - during removal of
the file from the image, as a success message 'ERRORCODE 1' was placed
on the stdout. If there was an issue with the file different message was
placed also on stdout. Luckily, that weird behaviour was fixed in 3.x
series of the Vice. So now we can remove unnecessary code.

Also, fixed two bugs for pyton3, introduced during new formats
development.
2019-09-20 18:40:54 +02:00
76daa17640 Added support for .d81 images. 2019-09-20 18:05:40 +02:00
3fa965de85 Added support for .d71 images 2019-09-20 16:05:35 +02:00
f8da7bf58e Moving offset calculation out of base class 2019-09-20 16:04:37 +02:00
eb57773166 Added subclass for D71 and D81 images 2019-09-20 16:03:47 +02:00
6236015e22 Reorganize the code, added constants. 2019-09-20 15:58:44 +02:00
6a455c6c55 Added stub for other than D64 formats. 2019-09-20 15:52:26 +02:00
45bde06dd3 Changed shebang back to python.
In modern linux distributions, executable "python" is a symlink to the
selected Python interpreter. There could be multiple versions installed
on the system, and appropriate tools on certain distributions exists for
selecting default one. There is no need for changing it to explicitly
"python3" executable.
2019-09-15 09:40:10 +02:00
03b6236463 Not provided subcommand is fatal on Python3. 2019-07-20 13:49:34 +02:00
711695be2b Added initial Python3 support 2018-02-13 20:19:02 +01:00
197ba7a707 Corrected shebang 2018-02-13 20:18:09 +01:00
58d3fce340 Added urls for sourcecode 2015-09-21 21:35:58 +02:00
d4e3794c1c Added screenshots and test image file 2015-09-03 19:27:16 +02:00
7c2e6f19a9 Moved changelog to readme file 2015-09-03 19:26:35 +02:00
fa81fc0b57 Added license and README file 2015-09-02 21:58:45 +02:00
3822ad2531 Better treatment of non-dos disc 2014-01-04 10:40:13 +01:00
909a42dd28 Gzip support 2013-11-12 09:13:08 +01:00
d44c9e28fa Correct version in docstring 2013-05-07 18:24:39 +02:00
0bdb6d8694 Added mkdir and run handling (or rather lack of handling :). Minor refactoring. 2013-05-07 18:23:10 +02:00
1346a2fcf8 Fixed bug with filenames started with a '-' sign. 2012-10-16 15:50:05 +02:00
fa3efe5f9f Fixed endless loop bug in Python D64 implementation 2012-09-30 14:51:36 +02:00
b61135b3de Re added missing method for correcting names 2012-09-24 21:17:00 +02:00
4ef4ec289e Fixed bug with unusual sector end 2012-09-24 21:14:39 +02:00
63a88e84a1 Fixed bug with slash in filenames 2012-09-24 21:12:11 +02:00
6 changed files with 520 additions and 216 deletions

24
LICENSE Normal file
View 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.

129
README.rst Normal file
View File

@@ -0,0 +1,129 @@
===================================================
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 2.7 or Python 3.6 or higher
* 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.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
media/test.d6z Normal file

Binary file not shown.

BIN
media/vice_dir.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

583
uc1541
View File

@@ -1,38 +1,25 @@
#! /usr/bin/env python
#!/usr/bin/env python
"""
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.
Changelog:
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-02
Version: 2.0
Date: 2020-06-28
Version: 3.4
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 "
@@ -40,45 +27,77 @@ 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 _ord(string_or_int):
"""
Implement d64 directory reader
Return an int value for the (possible) string passed in argument. This
function is for compatibility between python2 and python3, where single
element in byte string array is a string or an int respectively.
"""
CHAR_MAP = {32: ' ', 33: '!', 34: '"', 35: '#', 36: '$', 37: '%', 38: '&',
39: "'", 40: '(', 41: ')', 42: '*', 43: '+', 44: ',', 45: '-',
46: '.', 47: '/', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4',
53: '5', 54: '6', 55: '7', 56: '8', 57: '9', 59: ';', 60: '<',
61: '=', 62: '>', 63: '?', 64: '@', 65: 'a', 66: 'b', 67: 'c',
68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h', 73: 'i', 74: 'j',
75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o', 80: 'p', 81: 'q',
82: 'r', 83: 's', 84: 't', 85: 'u', 86: 'v', 87: 'w', 88: 'x',
89: 'y', 90: 'z', 91: '[', 93: ']', 97: 'A', 98: 'B', 99: 'C',
try:
return ord(string_or_int)
except TypeError:
return string_or_int
def _get_raw(dimage):
"""
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: '.',
47: '/', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5',
54: '6', 55: '7', 56: '8', 57: '9', 59: ';', 60: '<', 61: '=',
62: '>', 63: '?', 64: '@', 65: 'a', 66: 'b', 67: 'c', 68: 'd',
69: 'e', 70: 'f', 71: 'g', 72: 'h', 73: 'i', 74: 'j', 75: 'k',
76: 'l', 77: 'm', 78: 'n', 79: 'o', 80: 'p', 81: 'q', 82: 'r',
83: 's', 84: 't', 85: 'u', 86: 'v', 87: 'w', 88: 'x', 89: 'y',
90: 'z', 91: '[', 93: ']', 97: 'A', 98: 'B', 99: 'C',
100: 'D', 101: 'E', 102: 'F', 103: 'G', 104: 'H', 105: 'I',
106: 'J', 107: 'K', 108: 'L', 109: 'M', 110: 'N', 111: 'O',
112: 'P', 113: 'Q', 114: 'R', 115: 'S', 116: 'T', 117: 'U',
@@ -95,20 +114,19 @@ 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._sector_shift = 256
self.next_sector = 0
self.next_track = None
self._directory_contents = []
self._dir_contents = []
self._already_done = []
def _map_filename(self, string):
"""
@@ -117,16 +135,17 @@ class D64(object):
"""
filename = list()
in_fname = True
for chr_ in string:
character = D64.CHAR_MAP.get(ord(chr_), '?')
if _ord(chr_) == 160: # shift+space character; $a0
break
if in_fname:
if ord(chr_) == 160:
in_fname = False
else:
filename.append(character)
character = D64.CHAR_MAP.get(_ord(chr_), '?')
filename.append(character)
# special cases
if filename[0] == "-":
filename[0] = "?"
LOG.debug("string: ``%s'' mapped to: ``%s''", string,
"".join(filename))
@@ -138,22 +157,41 @@ class D64(object):
Return False if the chain ends, True otherwise
"""
if self.next_track == 0 and self.next_sector == 255:
# Well, self.next_sector _should_ have value $FF, but apparently there
# are the cases where it is not, therefore checking for that will not
# be performed and value of $00 on the next track will end the
# directory
if self.next_track == 0:
LOG.debug("End of directory")
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 + self._sector_shift]
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 = _ord(self.current_sector_data[0])
self.next_sector = _ord(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.
LOG.debug("Loop in track/sector pointer at %d,%d",
self.next_track, self.next_sector)
self._already_done = []
return False
self._already_done.append((self.next_track, self.next_sector))
LOG.debug("Next track: %s,%s", self.next_track, self.next_sector)
return True
@@ -165,39 +203,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 _directory_contents
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 = _ord(entry[2])
if ftype == 0: # deleted
sector = sector[32:]
@@ -205,17 +224,17 @@ class D64(object):
type_verbose = self._get_ftype(ftype)
protect = ord(entry[2]) & 64 and "<" or " "
protect = _ord(entry[2]) & 64 and "<" or " "
fname = entry[5:21]
if ftype == 'rel':
size = ord(entry[23])
size = _ord(entry[23])
else:
size = ord(entry[30]) + ord(entry[31]) * 226
size = _ord(entry[30]) + _ord(entry[31]) * 226
self._directory_contents.append({'fname': self._map_filename(fname),
'ftype': type_verbose,
'size': size,
'protect': protect})
self._dir_contents.append({'fname': self._map_filename(fname),
'ftype': type_verbose,
'size': size,
'protect': protect})
sector = sector[32:]
def list_dir(self):
@@ -226,7 +245,124 @@ class D64(object):
while self._go_to_next_sector():
self._harvest_entries()
return self._directory_contents
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):
@@ -242,19 +378,24 @@ 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 = []
def list(self):
"""
Output list contents of D64 image.
Convert filenames to be unix filesystem friendly
Convert filenames to be Unix filesystem friendly
Add suffix to show user what kind of file do he dealing with.
"""
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)
@@ -266,19 +407,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):
"""
@@ -286,37 +415,63 @@ 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):
"""
Copy file form the D64 image. Source filename has to be corrected,
since it's representaion differ from the real one inside D64 image.
since it's representation differ from the real one inside D64 image.
"""
LOG.info("Copy form D64 %s as %s", src, dst)
if not src.endswith(".prg"):
return "canot read"
return "cannot read"
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):
"""
Return filename with mapped characters, without .prg extension.
Characters like $, *, + in filenames are perfectly legal, but c1541
program seem to have issues with it while writing, so it will also be
replaced.
"""
char_map = {'|': "/",
"\\": "/",
"~": " ",
"$": "?",
"*": "?"}
if fname.lower().endswith(".prg"):
fname = fname[:-4]
new_fname = []
for char in fname:
trans = char_map.get(char)
new_fname.append(trans if trans else char)
return "".join(new_fname)
def _get_masked_fname(self, fname):
"""
Return masked filename with '?' jokers instead of non ASCII
characters, usefull for copying or deleting files with c1541. In case
characters, useful for copying or deleting files with c1541. In case
of several files with same name exists in directory, only first one
will be operative (first as appeared in directory).
Warning! If there are two different names but the only differenc is in
non-ASCII characters (some PET ASCII or controll characters) there is
Warning! If there are two different names but the only difference is in
non-ASCII characters (some PET ASCII or control characters) there is
a risk that one can remove both files.
"""
directory = self._get_dir()
@@ -334,8 +489,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"):
@@ -346,14 +502,18 @@ class Uc1541(object):
continue
display_name = ".".join([fname, ext])
pattern_name = self.pyd64[idx]['fname']
pattern_name = self.dirlist[idx]['fname']
if '/' in fname:
display_name = fname.replace('/', '|')
if '/' in display_name:
display_name = display_name.replace('/', '|')
# workaround for space at the beggining of the filename
if fname[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 = "----------"
@@ -364,7 +524,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
@@ -374,9 +534,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):
"""
@@ -387,29 +547,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 "
@@ -417,67 +593,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())