-
Notifications
You must be signed in to change notification settings - Fork 3
Examples
- UNIX-like shell is assumed except where noted
- a2kit must be installed and in the path
- Some examples require Python 3.8 or higher be installed and in the path
The simplest way to shuttle files around is with the smart copy. It is designed to give you what you most likely expect with minimal typing.
- extract file from disk image
a2kit cp /path/to/dimg.woz/basic.system /path/to/destination/directory
- extract matching files from disk image
a2kit cp "/path/to/dimg.woz/*.system" .- using dot-notation to specify current directory as target
- double-quotes suppress the bash glob expansion so that a2kit receives the
*
- recursive file extraction
a2kit cp "/path/to/dimg.woz/apps/**" .- all files go to the same destination directory
- write file to disk image with new name
a2kit cp /path/to/myfile.txt /path/to/myimg.2mg/myfile- with apple disks you may want to suppress the filename extension
- write matching files to disk image
a2kit cp /path/to/*.txt /path/to/myimg.2mg- in this case do not suppress the shell's glob expansion
- handle the load address
a2kit cp -a 768 /path/to/mybinary /path/to/myimg.2mg- only for host-to-image with some apple files
- copy from one disk image to another
a2kit cp /path/to/img1.woz/*.system /path/to/img2.woz- may require the file systems to match
This is a shell session to create a ProDOS disk with an Applesoft greeting program. For the moment, suppose that the Applesoft source code startup.bas and the file images for PRODOS and BASIC.SYSTEM are already in the working directory.
# first create the disk
a2kit mkdsk -o prodos -t woz2 -v demo -d demo.woz
# put the system file images on the disk
a2kit put -f prodos -t any -d demo.woz < prodos.json
a2kit put -f basic.system -t any -d demo.woz < basic.system.json
# tokenize and put the greeting program
a2kit tokenize -t atxt -a 2049 < startup.bas | a2kit put -t atok -f startup -d demo.wozThe source code startup.bas can be created in any text editor. As an example we might have
10 home
20 print "hello from my new disk"The file images can be obtained from another disk image containing the actual files. For example, if you already have a bootable disk image legit.woz, you can create the PRODOS file image like this:
cd /path/to/my/file/images
a2kit get -f prodos -t any -d legit.woz > prodos.jsonTo gather the paths to all the files on a disk
a2kit glob -d adventures.imd -f "**"The quotes prevent the shell from doing its own glob expansion, you can usually omit them on Windows. For ProDOS and FAT disks, you can omit the root path (remember on ProDOS this includes the volume name). For example,
a2kit glob -d adventures.po -f "/adventure.disk/fantasy/**/orc"
a2kit glob -d adventures.po -f "/*/fantasy/**/orc"
a2kit glob -d adventures.po -f "fantasy/**/orc"will all find orcs anywhere in the fantasy directory.
Here is a PowerShell script that deploys BASIC and assembly language files to a disk image. This could be used, e.g., to accelerate a workflow where files are updated in a modern editor, and tested in an emulator.
# PowerShell script deploying files to a disk image.
# This would be run from the local project directory.
# PS 7.4 doesn't need native wrappers anymore...hooray!
# (following is a directive)
#Requires -Version 7.4
# If you want to check a2kit version it can be done like this
$min_a2kit_vers = "3.7.0"
$a2kit_vers = (a2kit -V).Split()[1]
if ([Version]$a2kit_vers -lt [Version]$min_a2kit_vers) {
Write-Error ("requires a2kit v" + $min_a2kit_vers)
}
# Tell PowerShell we want to stop if any a2kit command fails
Set-Variable ErrorActionPreference "Stop"
Set-Variable PSNativeCommandUseErrorActionPreference $true
# Suppose we have a logical hard drive we are mounting in the emulator:
Set-Variable dimg ($env:USERPROFILE + "\OneDrive\path\to\DISKS\emulatorHD.po")
# Setup lists of files we want to move
Set-Variable prodosPath "programming/merlin/myproject/"
Set-Variable asmFiles @("myasm.s","mymacs.s","mymod.s")
Set-Variable basicFiles @("mymain","myutil")
# Overwriting is not allowed, so do precleaning.
foreach ($f in ($basicFiles + $asmFiles)) {
try { a2kit delete -d $dimg -f ($prodosPath + $f) } catch { Write-Warning ("no pre-existing " + $f) }
}
# Loop over the files and move them.
foreach ($f in $asmFiles) {
a2kit get -f $f | a2kit tokenize -t mtxt | a2kit put -d $dimg -f ($prodosPath + $f) -t mtok
}
foreach ($f in $basicFiles) {
a2kit get -f ($f + ".bas") | a2kit tokenize -a 2049 -t atxt | a2kit put -d $dimg -f ($prodosPath + $f) -t atok
}
# Catalog the project directory so we can see if it looks alright
a2kit catalog -d $dimg -f $prodosPathIf you wanted to cross-assemble you would replace the Merlin tokenization step with the cross-assembler, then a2kit put with -t bin.
The following python module is a simple Python front-end. This is used by some of the other examples, see below. Save this as a2kit.py in the directory with whatever script is importing it.
'''Simple example of a Python front-end. The backend interface is mirrored exactly.'''
import subprocess
import warnings
def verify(beg_vers: tuple, end_vers: tuple):
'''Exit if a2kit version falls outside range beg_vers..end_vers'''
full_vers = cmd(['-V']).decode('utf-8').split()[1].split('-')
if len(full_vers) > 1:
warnings.warn("this is a prerelease ({})".format(full_vers[1]))
vers = tuple(map(int, full_vers[0].split('.')))
if vers < beg_vers or vers >= end_vers:
print("a2kit version outside range",beg_vers,"..",end_vers)
exit(1)
def cmd(args, pipe_in=None):
'''run a CLI command as a subprocess'''
compl = subprocess.run(['a2kit']+args,input=pipe_in,capture_output=True,text=False)
if compl.returncode>0:
print(compl.stderr)
exit(1)
return compl.stdoutN.b. string inputs have to be encoded (e.g. using encode('utf-8')), and string outputs have to decoded (e.g. using decode('utf-8')).
In this example we use a Python script to create a bootable CP/M disk for a Kaypro 4. To make a bootable CP/M disk, you will generally need to know some details about how the target vendor chose to lay out their format. In the case of the Kaypro 4, the tricky thing is there are reserved blocks in the user area.
'''Script to make a bootable Kaypro DSDD disk.
This illustrates some low level copying.'''
import sys
import a2kit # front end, see above
a2kit.verify((4,0,0),(5,0,0))
# Parse command line
if len(sys.argv)!=3:
print("usage: python "+sys.argv[0]+" <path in> <path out>")
print("<path in> path to an existing bootable disk image")
print("<path out> path to the new disk image")
exit(1)
path_in = sys.argv[1]
path_out = sys.argv[2]
# Make a blank image, this sets up the track layout etc. for a Kaypro DSDD disk.
a2kit.cmd(["mkdsk","-o","cpm2","-t","imd","-k","5.25in-kay-dsdd","-d", path_out])
# Copy the reserved track - it is on cylinder 0, side 0.
# Kaypro DSDD has sectors 0-9 on side 0, sectors 10-19 on side 1
# Important note: in this scheme, the head map value is always 0.
print("copying reserved track")
chs = "0,0,0..10"
sec_data = a2kit.cmd(["get", "-f", chs, "-t", "sec", "-d", path_in])
a2kit.cmd(["put", "-f", chs, "-t", "sec", "-d", path_out], sec_data)
# This format has a reserved block at block 1. We need to copy that too.
# (this could also be done by copying cylinder 0, side 1, sectors 14-17)
print("copying reserved block")
block_data = a2kit.cmd(["get", "-f", "1", "-t", "block", "-d", path_in])
a2kit.cmd(["put", "-f", "1", "-t", "block", "-d", path_out], block_data)
# Suppose we want ED.COM on this disk. Copy it over.
# Obviously this means the source disk must have ED.COM.
# Note the use of the `any` type: we are sending a file image through the pipe.
print("copying files we want")
ed_dot_com = a2kit.cmd(["get", "-f", "ed.com", "-t", "any", "-d", path_in])
a2kit.cmd(["put", "-f", "ed.com", "-t", "any", "-d", path_out], ed_dot_com)Here is a reasonably fast way to locate multiple occurrences of a pattern within a disk image.
'''Script to search any supported disk image for a pattern in the decoded data.
The search is bounded by ranges of cylinders, heads, and sectors.
The bounding cylinder and head are *geometrically* ordered.'''
import sys
import json
import a2kit # front end, see above
a2kit.verify((4,0,0),(5,0,0))
# Parse command line
if len(sys.argv)!=6:
print("usage: python "+sys.argv[0]+" <path> <cyl beg>,<cyl end> <head beg>,<head end> <sec beg>,<sec end> <pattern>")
print("pattern can be comma-delimited decimals or an ASCII string")
exit(1)
img_path = sys.argv[1]
cyl_rng = range(int(sys.argv[2].split(',')[0]),int(sys.argv[2].split(',')[1]))
head_rng = range(int(sys.argv[3].split(',')[0]),int(sys.argv[3].split(',')[1]))
sec_rng = range(int(sys.argv[4].split(',')[0]),int(sys.argv[4].split(',')[1]))
raw_patt = sys.argv[5]
try:
search_pattern = list(map(int,raw_patt.split(',')))
except:
search_pattern = list(map(ord,raw_patt))
def search(dat: bytes,addr,c,h,s):
matches = 0
for i,val in enumerate(list(dat)):
if search_pattern[matches]==val:
matches += 1
else:
matches = 0
if matches==len(search_pattern):
print("match completed:")
print(" geometric CHS =",[c,h,s])
print(" physical addr =",addr)
print(" offset =",hex(i+1-len(search_pattern)))
matches = 0
# Strategy is to gather data by track, this is quite a bit faster than gathering by sector
geometry = json.loads(a2kit.cmd(["geometry","-d",img_path]))
for trk in geometry["tracks"]:
c = trk["cylinder"] # geometric order
h = trk["head"] # geometric order
if c in cyl_rng and h in head_rng:
search_range = str(c) + ".." + str(c+1) + ","
search_range += str(h) + ".." + str(h+1) + ","
search_range += str(sec_rng.start) + ".." + str(sec_rng.stop)
dat = a2kit.cmd(["get","-d",img_path,"-t","sec","-f",search_range])
if type(trk["solution"]) == dict:
addr_map = trk["solution"]["addr_map"]
size_map = trk["solution"]["size_map"]
addr_type = trk["solution"]["addr_type"]
for i in range(len(addr_map)):
l = size_map[i]
addr_str = addr_map[i]
s_idx = addr_type.find("S")
s = int(addr_str[s_idx*2:s_idx*2+2],16)
if s in sec_rng:
search(dat[l*(s-sec_rng.start): l*(s-sec_rng.start+1)],addr_str,c,h,s)
else:
print("track was",trk["solution"])Converting an image is a matter of creating an output image and then copying the data from the input image. The following are examples of a general pattern that can be applied to any two compatible images.
Note the file system and volume name of the destination don't matter, as everything is overwritten by the copy.
a2kit mkdsk -o prodos -t woz2 -v x -d dst.woz
a2kit get -d src.do -t sec -f 0..35,0,0..16 | a2kit put -d dst.woz -t sec -f 0..35,0,0..16When copying by sector, avoid images where order is variable (DO is fine, DSK may not be).
For this you must use blocks, because there might be a logical volume involved. The ordering of the source disk doesn't matter, the destination will be DOS ordered either way.
a2kit mkdsk -o prodos -t do -v x -d dst.dsk
a2kit get -d src.dsk -t block -f 0..280 | a2kit put -d dst.dsk -t block -f 0..280What is going on under the hood is that by using blocks, we force a2kit to solve the file system, which in turn lets us figure out the ordering.
This script was developed before the smart copy was implemented. However it may still be useful because it can recreate the directory structure of the source at the destination, which the smart copy cannot yet do.
'''Easily extract files from a variety of retro disk images.
Files are put in a friendly format if possible.
Example: `python extract.py mydisk.woz path/to/my/target
Scope:
* Apple DOS, ProDOS, Apple Pascal, CP/M (various), MS-DOS (or other FAT)
* Applesoft BASIC, Integer BASIC, Pascal, various text
* 2MG, IMD, NIB, TD0, WOZ, DO/PO/DSK/IMG/IMA
Dependencies:
* a2kit CLI - `cargo install a2kit` or download from https://github.com/dfgordon/a2kit/releases/latest
Notes:
* CP/M user numbers will become directories'''
import sys
import os
import pathlib
import glob
import json
import subprocess
import platform
# simple front end for a2kit
def cmd(args, pipe_in=None, tolerant=False):
'''run a CLI command as a subprocess'''
compl = subprocess.run(['a2kit']+args,input=pipe_in,capture_output=True,text=False)
if compl.returncode>0:
if not tolerant:
raise ValueError(str(compl.stderr))
else:
return compl.stdout,compl.stderr
return compl.stdout,None
# check dependencies
if tuple(map(int,platform.python_version_tuple())) < (3,8,0):
print("requires python 3.8")
exit(1)
vers = tuple(map(int,cmd(["-V"])[0].decode('utf-8').split()[1].split('-')[0].split('.')))
if vers < (4,0,0) or vers >= (5,0,0):
print("a2kit version out of range")
exit(1)
# parse command line
if len(sys.argv)!=3:
print("usage: python "+sys.argv[0]+" <img_path> <target_dir>")
print("<path> path to disk image")
print("<target_dir> where to put the files")
exit(1)
dimg = sys.argv[1]
target = pathlib.Path(sys.argv[2])
failures = []
# verify target directory is empty
os.makedirs(target,exist_ok=True)
cleanstr = glob.glob(str(target/"*"))
if len(cleanstr)>0:
print("target directory needs to be empty")
exit(1)
print("glob the disk image...")
all_paths = cmd(["glob","-f","**","-d",dimg])[0]
print("gather all file images...")
all_files,err = cmd(["mget","-d",dimg],all_paths,True)
if err:
print("mget failed")
exit(1)
print("unpack and save...")
all_files = json.loads(all_files)
for file in all_files:
print(file["full_path"])
file_data,err = cmd(["unpack","-t","auto"],bytearray(json.dumps(file),"utf8"),True)
if file_data:
fs_type = int(file["fs_type"],16)
if file["file_system"] == "prodos" and fs_type == 0xfa:
file_data,err = cmd(["detokenize","-t","itok"],file_data,True)
if file["file_system"] == "prodos" and fs_type == 0xfc:
file_data,err = cmd(["detokenize","-t","atok"],file_data,True)
if file["file_system"] == "a2 dos" and fs_type & 0x7f == 1:
file_data,err = cmd(["detokenize","-t","itok"],file_data,True)
if file["file_system"] == "a2 dos" and fs_type & 0x7f == 2:
file_data,err = cmd(["detokenize","-t","atok"],file_data,True)
if err!=None:
failures += [file["full_path"]+": "+str(err)]
elif file_data:
dest_path = file["full_path"]
if file["file_system"] == "cpm":
dest_path = dest_path.replace(":","/")
while dest_path[0] == "/":
dest_path = dest_path[1:]
host_path = target / pathlib.Path(dest_path)
os.makedirs(host_path.parent,exist_ok=True)
with open(host_path,'b+w') as f:
f.write(file_data)
else:
raise RuntimeError("unreachable was reached")
if len(failures)>0:
print()
print("ERROR REPORT")
print("------------")
for err in failures:
print(err)