diskstack 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diskstack/__init__.py +5 -0
- diskstack/_vendor/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/COPYING +24 -0
- diskstack/_vendor/greaseweazle/__init__.py +2 -0
- diskstack/_vendor/greaseweazle/codec/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/amiga/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/amiga/amigados.py +221 -0
- diskstack/_vendor/greaseweazle/codec/apple2/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/apple2/apple2_gcr.py +223 -0
- diskstack/_vendor/greaseweazle/codec/bitcell.py +117 -0
- diskstack/_vendor/greaseweazle/codec/codec.py +362 -0
- diskstack/_vendor/greaseweazle/codec/commodore/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/commodore/c64_gcr.py +229 -0
- diskstack/_vendor/greaseweazle/codec/datageneral/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/datageneral/datageneral.py +211 -0
- diskstack/_vendor/greaseweazle/codec/hp/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/hp/hp_mmfm.py +258 -0
- diskstack/_vendor/greaseweazle/codec/ibm/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/ibm/ibm.py +1005 -0
- diskstack/_vendor/greaseweazle/codec/macintosh/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/macintosh/mac_gcr.py +238 -0
- diskstack/_vendor/greaseweazle/codec/micropolis/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/micropolis/micropolis.py +233 -0
- diskstack/_vendor/greaseweazle/codec/northstar/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/northstar/northstar.py +210 -0
- diskstack/_vendor/greaseweazle/data/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/data/diskdefs.cfg +81 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_acorn.cfg +119 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_akai.cfg +27 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_amiga.cfg +17 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_apple2.cfg +31 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_apricot.cfg +25 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_atari.cfg +33 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_atarist.cfg +76 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_coco.cfg +98 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_commodore.cfg +127 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_datageneral.cfg +8 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_dec.cfg +37 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_dragon.cfg +61 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_eagle.cfg +25 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_ensoniq.cfg +39 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_epson.cfg +95 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_gem.cfg +15 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_hp.cfg +25 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_ibm.cfg +140 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_kaypro.cfg +58 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_luxor.cfg +96 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_mac.cfg +71 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_micropolis.cfg +73 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_mm1.cfg +69 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_msx.cfg +45 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_northstar.cfg +37 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_occ1.cfg +23 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_olivetti.cfg +18 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_pc98.cfg +69 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_raw.cfg +25 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_rm.cfg +98 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_sci.cfg +15 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_sega.cfg +13 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_sharp.cfg +13 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_thomson.cfg +78 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_tsc.cfg +81 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_xerox.cfg +59 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_zx.cfg +245 -0
- diskstack/_vendor/greaseweazle/error.py +19 -0
- diskstack/_vendor/greaseweazle/flux.py +315 -0
- diskstack/_vendor/greaseweazle/image/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/image/a2r.py +133 -0
- diskstack/_vendor/greaseweazle/image/acorn.py +28 -0
- diskstack/_vendor/greaseweazle/image/adf.py +15 -0
- diskstack/_vendor/greaseweazle/image/apple2.py +18 -0
- diskstack/_vendor/greaseweazle/image/apridisk.py +107 -0
- diskstack/_vendor/greaseweazle/image/caps.py +464 -0
- diskstack/_vendor/greaseweazle/image/d64.py +51 -0
- diskstack/_vendor/greaseweazle/image/d81.py +28 -0
- diskstack/_vendor/greaseweazle/image/d88.py +211 -0
- diskstack/_vendor/greaseweazle/image/dcp.py +45 -0
- diskstack/_vendor/greaseweazle/image/dim.py +53 -0
- diskstack/_vendor/greaseweazle/image/dmk.py +166 -0
- diskstack/_vendor/greaseweazle/image/dsk.py +31 -0
- diskstack/_vendor/greaseweazle/image/edsk.py +591 -0
- diskstack/_vendor/greaseweazle/image/fd.py +23 -0
- diskstack/_vendor/greaseweazle/image/fdi.py +41 -0
- diskstack/_vendor/greaseweazle/image/hdm.py +15 -0
- diskstack/_vendor/greaseweazle/image/hfe.py +742 -0
- diskstack/_vendor/greaseweazle/image/image.py +130 -0
- diskstack/_vendor/greaseweazle/image/imd.py +201 -0
- diskstack/_vendor/greaseweazle/image/img.py +111 -0
- diskstack/_vendor/greaseweazle/image/kryoflux.py +304 -0
- diskstack/_vendor/greaseweazle/image/mgt.py +15 -0
- diskstack/_vendor/greaseweazle/image/msa.py +153 -0
- diskstack/_vendor/greaseweazle/image/nfd.py +113 -0
- diskstack/_vendor/greaseweazle/image/nsi.py +47 -0
- diskstack/_vendor/greaseweazle/image/scp.py +471 -0
- diskstack/_vendor/greaseweazle/image/sf7.py +15 -0
- diskstack/_vendor/greaseweazle/image/sharp2d.py +15 -0
- diskstack/_vendor/greaseweazle/image/td0.py +141 -0
- diskstack/_vendor/greaseweazle/image/xdf.py +15 -0
- diskstack/_vendor/greaseweazle/optimised/__init__.py +22 -0
- diskstack/_vendor/greaseweazle/tools/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/tools/util.py +133 -0
- diskstack/_vendor/greaseweazle/track.py +494 -0
- diskstack/cache.py +210 -0
- diskstack/candidates.py +396 -0
- diskstack/cli.py +348 -0
- diskstack/errors.py +5 -0
- diskstack/filler.py +29 -0
- diskstack/formats.py +381 -0
- diskstack/parallel.py +73 -0
- diskstack/report.py +353 -0
- diskstack/stack.py +317 -0
- diskstack-1.0.0.dist-info/METADATA +329 -0
- diskstack-1.0.0.dist-info/RECORD +119 -0
- diskstack-1.0.0.dist-info/WHEEL +5 -0
- diskstack-1.0.0.dist-info/entry_points.txt +2 -0
- diskstack-1.0.0.dist-info/licenses/LICENSE +21 -0
- diskstack-1.0.0.dist-info/licenses/NOTICE +55 -0
- diskstack-1.0.0.dist-info/licenses/diskstack/_vendor/greaseweazle/COPYING +24 -0
- diskstack-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# greaseweazle/image/acorn.py
|
|
2
|
+
#
|
|
3
|
+
# Written & released by Keir Fraser <keir.xen@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# This is free and unencumbered software released into the public domain.
|
|
6
|
+
# See the file COPYING for more details, or visit <http://unlicense.org>.
|
|
7
|
+
|
|
8
|
+
from diskstack._vendor.greaseweazle.image.img import IMG
|
|
9
|
+
|
|
10
|
+
class SSD(IMG):
|
|
11
|
+
default_format = 'acorn.dfs.ss'
|
|
12
|
+
sequential = True
|
|
13
|
+
|
|
14
|
+
class DSD(IMG):
|
|
15
|
+
default_format = 'acorn.dfs.ds'
|
|
16
|
+
|
|
17
|
+
class ADS(IMG):
|
|
18
|
+
default_format = 'acorn.adfs.160'
|
|
19
|
+
|
|
20
|
+
class ADM(IMG):
|
|
21
|
+
default_format = 'acorn.adfs.320'
|
|
22
|
+
|
|
23
|
+
class ADL(IMG):
|
|
24
|
+
default_format = 'acorn.adfs.640'
|
|
25
|
+
|
|
26
|
+
# Local variables:
|
|
27
|
+
# python-indent: 4
|
|
28
|
+
# End:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# greaseweazle/image/adf.py
|
|
2
|
+
#
|
|
3
|
+
# Written & released by Keir Fraser <keir.xen@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# This is free and unencumbered software released into the public domain.
|
|
6
|
+
# See the file COPYING for more details, or visit <http://unlicense.org>.
|
|
7
|
+
|
|
8
|
+
from diskstack._vendor.greaseweazle.image.img import IMG
|
|
9
|
+
|
|
10
|
+
class ADF(IMG):
|
|
11
|
+
default_format = 'amiga.amigados'
|
|
12
|
+
|
|
13
|
+
# Local variables:
|
|
14
|
+
# python-indent: 4
|
|
15
|
+
# End:
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# greaseweazle/image/apple2.py
|
|
2
|
+
#
|
|
3
|
+
# Written & released by Keir Fraser <keir.xen@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# This is free and unencumbered software released into the public domain.
|
|
6
|
+
# See the file COPYING for more details, or visit <http://unlicense.org>.
|
|
7
|
+
|
|
8
|
+
from diskstack._vendor.greaseweazle.image.img import IMG
|
|
9
|
+
|
|
10
|
+
class DO(IMG):
|
|
11
|
+
default_format = 'apple2.appledos.140'
|
|
12
|
+
|
|
13
|
+
class PO(IMG):
|
|
14
|
+
default_format = 'apple2.prodos.140'
|
|
15
|
+
|
|
16
|
+
# Local variables:
|
|
17
|
+
# python-indent: 4
|
|
18
|
+
# End:
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# greaseweazle/tools/apridisk.py
|
|
2
|
+
#
|
|
3
|
+
# Released by Keir Fraser <keir.xen@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# This is free and unencumbered software released into the public domain.
|
|
6
|
+
# See the file COPYING for more details, or visit <http://unlicense.org>.
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
from diskstack._vendor.greaseweazle import error
|
|
11
|
+
from diskstack._vendor.greaseweazle.codec import codec
|
|
12
|
+
from diskstack._vendor.greaseweazle.image.img import IMG
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Apridisk(IMG):
|
|
16
|
+
|
|
17
|
+
def from_bytes(self, dat: bytes) -> None:
|
|
18
|
+
error.check(len(dat) >= 128, "Apridisk file missing 128 byte header")
|
|
19
|
+
dat = dat[128:]
|
|
20
|
+
|
|
21
|
+
records = ApridiskRecord.split_apridisk_file(dat, self.fmt)
|
|
22
|
+
records.sort(key = lambda h : h.ord)
|
|
23
|
+
|
|
24
|
+
data = bytes([i for r in records for i in r.expand_record()])
|
|
25
|
+
super().from_bytes(data)
|
|
26
|
+
|
|
27
|
+
class ApridiskRecord:
|
|
28
|
+
def __init__(self, data: bytes, fmt: codec.DiskDef):
|
|
29
|
+
error.check(len(data) >= 16, "Apridisk record contains no section header")
|
|
30
|
+
|
|
31
|
+
self.fmt = fmt
|
|
32
|
+
self.itm_type = ApridiskSecType.from_bytes(data[:4])
|
|
33
|
+
self.header_size = int.from_bytes(data[6:8], byteorder="little")
|
|
34
|
+
self.data_size = int.from_bytes(data[8:12], byteorder="little")
|
|
35
|
+
|
|
36
|
+
self.record_size = self.header_size + self.data_size
|
|
37
|
+
error.check(len(data) >= self.record_size,
|
|
38
|
+
f"Apridisk file ends before end of record. ")
|
|
39
|
+
self.data = data[self.header_size : self.record_size]
|
|
40
|
+
|
|
41
|
+
self._match_pos(data)
|
|
42
|
+
self._match_compression(data)
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def split_apridisk_file(file: bytes, fmt: codec.DiskDef) -> list[ApridiskRecord]:
|
|
46
|
+
res: list[ApridiskRecord] = []
|
|
47
|
+
byte = 0
|
|
48
|
+
while file:
|
|
49
|
+
record = ApridiskRecord(file, fmt)
|
|
50
|
+
res.append(record)
|
|
51
|
+
byte += record.record_size
|
|
52
|
+
file = file[record.record_size:]
|
|
53
|
+
return res
|
|
54
|
+
|
|
55
|
+
def expand_record(self) -> bytes:
|
|
56
|
+
if self.itm_type != ApridiskSecType.SECTOR:
|
|
57
|
+
return b''
|
|
58
|
+
if self.compression:
|
|
59
|
+
return bytes([self.byte for _ in range(0, self.count)])
|
|
60
|
+
return self.data
|
|
61
|
+
|
|
62
|
+
def _match_compression(self, data: bytes):
|
|
63
|
+
compression = int.from_bytes(data[4:6], byteorder="little")
|
|
64
|
+
error.check(compression in [0x9E90, 0x3E5A],
|
|
65
|
+
f"Apridisk unknown compression scheme: {compression}")
|
|
66
|
+
self.compression = compression == 0x3E5A
|
|
67
|
+
|
|
68
|
+
if self.compression:
|
|
69
|
+
error.check(len(self.data) == 3,
|
|
70
|
+
f"Apridisk compressed section length must be 3, length: {len(self.data)}")
|
|
71
|
+
self.count = int.from_bytes(self.data[0:2], byteorder="little")
|
|
72
|
+
self.byte = self.data[2]
|
|
73
|
+
|
|
74
|
+
def _match_pos(self, data: bytes):
|
|
75
|
+
error.check(data[12] in range(0, self.fmt.heads or 2),
|
|
76
|
+
f"Apridisk heads out of range (max {self.fmt.heads or 2}): {hex(data[12])}")
|
|
77
|
+
self.head = data[12]
|
|
78
|
+
|
|
79
|
+
error.check(data[13] in range(1, 19) or self.itm_type != ApridiskSecType.SECTOR,
|
|
80
|
+
f"Apridisk sector out of range (must be 1-9): {data[13]}")
|
|
81
|
+
self.sector = data[13]
|
|
82
|
+
|
|
83
|
+
cyl = int.from_bytes(data[14:16], byteorder="little")
|
|
84
|
+
error.check(cyl in range(0, self.fmt.cyls or 80),
|
|
85
|
+
f"Apridisk cylinder out of range for chosen format (max {self.fmt.cyls}): {cyl}")
|
|
86
|
+
self.cyl = cyl
|
|
87
|
+
|
|
88
|
+
# Used for sorting
|
|
89
|
+
self.ord = self.sector + (self.head * 20) + (self.cyl * 100)
|
|
90
|
+
|
|
91
|
+
class ApridiskSecType(Enum):
|
|
92
|
+
DELETED = 0xE31D0000
|
|
93
|
+
SECTOR = 0xE31D0001
|
|
94
|
+
COMMENT = 0xE31D0002
|
|
95
|
+
CREATOR = 0xE31D0003
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def from_bytes(b: bytes) -> ApridiskSecType:
|
|
99
|
+
num = int.from_bytes(b[:4], byteorder="little")
|
|
100
|
+
try:
|
|
101
|
+
return ApridiskSecType(num)
|
|
102
|
+
except:
|
|
103
|
+
raise error.Fatal(f"Unknown Apridisk sector type: {hex(num)}")
|
|
104
|
+
|
|
105
|
+
# Local variables:
|
|
106
|
+
# python-indent: 4
|
|
107
|
+
# End:
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
# greaseweazle/image/caps.py
|
|
2
|
+
#
|
|
3
|
+
# Written & released by Keir Fraser <keir.xen@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# This is free and unencumbered software released into the public domain.
|
|
6
|
+
# See the file COPYING for more details, or visit <http://unlicense.org>.
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from typing import cast, List, Tuple, Optional, Generator
|
|
10
|
+
|
|
11
|
+
import os, sys
|
|
12
|
+
import platform
|
|
13
|
+
import ctypes as ct
|
|
14
|
+
import itertools as it
|
|
15
|
+
from bitarray import bitarray
|
|
16
|
+
from diskstack._vendor.greaseweazle.track import MasterTrack, PLLTrack
|
|
17
|
+
from diskstack._vendor.greaseweazle.flux import Flux
|
|
18
|
+
from diskstack._vendor.greaseweazle import error
|
|
19
|
+
from .image import Image, OptDict
|
|
20
|
+
|
|
21
|
+
class CapsDateTimeExt(ct.Structure):
|
|
22
|
+
_pack_ = 1
|
|
23
|
+
_fields_ = [
|
|
24
|
+
('year', ct.c_uint),
|
|
25
|
+
('month', ct.c_uint),
|
|
26
|
+
('day', ct.c_uint),
|
|
27
|
+
('hour', ct.c_uint),
|
|
28
|
+
('min', ct.c_uint),
|
|
29
|
+
('sec', ct.c_uint),
|
|
30
|
+
('tick', ct.c_uint)]
|
|
31
|
+
|
|
32
|
+
class CapsImageInfo(ct.Structure):
|
|
33
|
+
platform_name = [
|
|
34
|
+
"N/A", "Amiga", "Atari ST", "IBM PC", "Amstrad CPC",
|
|
35
|
+
"Spectrum", "Sam Coupe", "Archimedes", "C64", "Atari (8-bit)" ]
|
|
36
|
+
_pack_ = 1
|
|
37
|
+
_fields_ = [
|
|
38
|
+
('type', ct.c_uint), # image type
|
|
39
|
+
('release', ct.c_uint), # release ID
|
|
40
|
+
('revision', ct.c_uint), # release revision ID
|
|
41
|
+
('mincylinder', ct.c_uint), # lowest cylinder number
|
|
42
|
+
('maxcylinder', ct.c_uint), # highest cylinder number
|
|
43
|
+
('minhead', ct.c_uint), # lowest head number
|
|
44
|
+
('maxhead', ct.c_uint), # highest head number
|
|
45
|
+
('crdt', CapsDateTimeExt), # image creation date.time
|
|
46
|
+
('platform', ct.c_uint * 4)] # intended platform(s)
|
|
47
|
+
|
|
48
|
+
class CapsTrackInfoT2(ct.Structure):
|
|
49
|
+
_pack_ = 1
|
|
50
|
+
_fields_ = [
|
|
51
|
+
('type', ct.c_uint), # track type
|
|
52
|
+
('cylinder', ct.c_uint), # cylinder#
|
|
53
|
+
('head', ct.c_uint), # head#
|
|
54
|
+
('sectorcnt', ct.c_uint), # available sectors
|
|
55
|
+
('sectorsize', ct.c_uint), # sector size, unused
|
|
56
|
+
('trackbuf', ct.POINTER(ct.c_ubyte)), # track buffer memory
|
|
57
|
+
('tracklen', ct.c_uint), # track buffer memory length
|
|
58
|
+
('timelen', ct.c_uint), # timing buffer length
|
|
59
|
+
('timebuf', ct.POINTER(ct.c_uint)), # timing buffer
|
|
60
|
+
('overlap', ct.c_int), # overlap position
|
|
61
|
+
('startbit', ct.c_uint), # start position of the decoding
|
|
62
|
+
('wseed', ct.c_uint), # weak bit generator data
|
|
63
|
+
('weakcnt', ct.c_uint)] # number of weak data areas
|
|
64
|
+
|
|
65
|
+
class CapsSectorInfo(ct.Structure):
|
|
66
|
+
_pack_ = 1
|
|
67
|
+
_fields_ = [
|
|
68
|
+
('descdatasize', ct.c_uint), # data size in bits from IPF descriptor
|
|
69
|
+
('descgapsize', ct.c_uint), # gap size in bits from IPF descriptor
|
|
70
|
+
('datasize', ct.c_uint), # data size in bits from decoder
|
|
71
|
+
('gapsize', ct.c_uint), # gap size in bits from decoder
|
|
72
|
+
('datastart', ct.c_uint), # data start pos in bits from decoder
|
|
73
|
+
('gapstart', ct.c_uint), # gap start pos in bits from decoder
|
|
74
|
+
('gapsizews0', ct.c_uint), # gap size before write splice
|
|
75
|
+
('gapsizews1', ct.c_uint), # gap size after write splice
|
|
76
|
+
('gapws0mode', ct.c_uint), # gap size mode before write splice
|
|
77
|
+
('gapws1mode', ct.c_uint), # gap size mode after write splice
|
|
78
|
+
('celltype', ct.c_uint), # bitcell type
|
|
79
|
+
('enctype', ct.c_uint)] # encoder type
|
|
80
|
+
|
|
81
|
+
class CapsDataInfo(ct.Structure):
|
|
82
|
+
_pack_ = 1
|
|
83
|
+
_fields_ = [
|
|
84
|
+
('type', ct.c_uint), # data type
|
|
85
|
+
('start', ct.c_uint), # start position
|
|
86
|
+
('size', ct.c_uint)] # size in bits
|
|
87
|
+
|
|
88
|
+
class DI_LOCK:
|
|
89
|
+
INDEX = 1<<0
|
|
90
|
+
ALIGN = 1<<1
|
|
91
|
+
DENVAR = 1<<2
|
|
92
|
+
DENAUTO = 1<<3
|
|
93
|
+
DENNOISE = 1<<4
|
|
94
|
+
NOISE = 1<<5
|
|
95
|
+
NOISEREV = 1<<6
|
|
96
|
+
MEMREF = 1<<7
|
|
97
|
+
UPDATEFD = 1<<8
|
|
98
|
+
TYPE = 1<<9
|
|
99
|
+
DENALT = 1<<10
|
|
100
|
+
OVLBIT = 1<<11
|
|
101
|
+
TRKBIT = 1<<12
|
|
102
|
+
NOUPDATE = 1<<13
|
|
103
|
+
SETWSEED = 1<<14
|
|
104
|
+
def_flags = (DENVAR | UPDATEFD | NOUPDATE | TYPE | OVLBIT | TRKBIT)
|
|
105
|
+
|
|
106
|
+
RangeList = List[Tuple[int,int]]
|
|
107
|
+
|
|
108
|
+
class CAPSTrackInfo(CapsTrackInfoT2):
|
|
109
|
+
|
|
110
|
+
class NoTrack(Exception):
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
def __init__(self, image: CAPS, cyl: int, head: int) -> None:
|
|
114
|
+
pi = image.pi
|
|
115
|
+
if head < pi.minhead or head > pi.maxhead:
|
|
116
|
+
raise CAPSTrackInfo.NoTrack()
|
|
117
|
+
if cyl < pi.mincylinder or cyl > pi.maxcylinder:
|
|
118
|
+
raise CAPSTrackInfo.NoTrack()
|
|
119
|
+
|
|
120
|
+
ti = self
|
|
121
|
+
CapsTrackInfoT2.__init__(ti, 2)
|
|
122
|
+
res = image.lib.CAPSLockTrack(ct.byref(ti), image.iid,
|
|
123
|
+
cyl, head, DI_LOCK.def_flags)
|
|
124
|
+
error.check(res == 0, "Could not lock CAPS track %d.%d" % (cyl, head))
|
|
125
|
+
|
|
126
|
+
if not ti.trackbuf:
|
|
127
|
+
raise CAPSTrackInfo.NoTrack() # unformatted/empty
|
|
128
|
+
|
|
129
|
+
carray_type = ct.c_ubyte * ((ti.tracklen+7)//8)
|
|
130
|
+
carray = carray_type.from_address(
|
|
131
|
+
ct.addressof(ti.trackbuf.contents))
|
|
132
|
+
bits = bitarray(endian='big')
|
|
133
|
+
bits.frombytes(bytes(carray))
|
|
134
|
+
bits = bits = bits[:ti.tracklen]
|
|
135
|
+
|
|
136
|
+
ticks: Optional[List[float]] = None
|
|
137
|
+
if ti.timebuf:
|
|
138
|
+
carray_type = ct.c_uint * ti.timelen
|
|
139
|
+
carray = carray_type.from_address(
|
|
140
|
+
ct.addressof(ti.timebuf.contents))
|
|
141
|
+
# Unpack the per-byte timing info into per-bitcell
|
|
142
|
+
ticks = []
|
|
143
|
+
for i in carray:
|
|
144
|
+
for j in range(8):
|
|
145
|
+
ticks.append(i)
|
|
146
|
+
# Pad the timing info with normal cell lengths as necessary
|
|
147
|
+
for j in range(len(carray)*8, ti.tracklen):
|
|
148
|
+
ticks.append(1000)
|
|
149
|
+
# Clip the timing info, if necessary.
|
|
150
|
+
ticks = ticks = ticks[:ti.tracklen]
|
|
151
|
+
|
|
152
|
+
ti.bits = bits
|
|
153
|
+
ti.ticks = ticks
|
|
154
|
+
|
|
155
|
+
# We don't really have access to the bitrate. It depends on RPM.
|
|
156
|
+
# So we assume a rotation rate of 300 RPM (5 rev/sec).
|
|
157
|
+
ti.rpm = 300
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class CAPS(Image):
|
|
161
|
+
|
|
162
|
+
iid: int
|
|
163
|
+
pi: CapsImageInfo
|
|
164
|
+
imagetype: str
|
|
165
|
+
|
|
166
|
+
read_only = True
|
|
167
|
+
|
|
168
|
+
def __init__(self, name: str, _fmt) -> None:
|
|
169
|
+
self.filename = name
|
|
170
|
+
self.lib = get_libcaps()
|
|
171
|
+
|
|
172
|
+
def __del__(self) -> None:
|
|
173
|
+
try:
|
|
174
|
+
self.lib.CAPSUnlockAllTracks(self.iid)
|
|
175
|
+
self.lib.CAPSUnlockImage(self.iid)
|
|
176
|
+
self.lib.CAPSRemImage(self.iid)
|
|
177
|
+
del(self.iid)
|
|
178
|
+
except AttributeError:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
def __str__(self) -> str:
|
|
182
|
+
raise NotImplementedError
|
|
183
|
+
|
|
184
|
+
def get_track(self, cyl: int, head: int) -> Optional[MasterTrack]:
|
|
185
|
+
raise NotImplementedError
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def from_file(cls, name: str, _fmt, opts: OptDict) -> Image:
|
|
189
|
+
|
|
190
|
+
caps = cls(name, _fmt)
|
|
191
|
+
caps.apply_r_opts(opts)
|
|
192
|
+
errprefix = f'CAPS: {cls.imagetype}'
|
|
193
|
+
|
|
194
|
+
caps.iid = caps.lib.CAPSAddImage()
|
|
195
|
+
error.check(caps.iid >= 0,
|
|
196
|
+
f"{errprefix}: Could not create image container")
|
|
197
|
+
cname = ct.c_char_p(name.encode())
|
|
198
|
+
res = caps.lib.CAPSLockImage(caps.iid, cname)
|
|
199
|
+
error.check(res == 0,
|
|
200
|
+
f"{errprefix}: Could not open image '{name}'")
|
|
201
|
+
res = caps.lib.CAPSLoadImage(caps.iid, DI_LOCK.def_flags)
|
|
202
|
+
error.check(res == 0,
|
|
203
|
+
f"{errprefix}: Could not load image '%s'" % name)
|
|
204
|
+
caps.pi = CapsImageInfo()
|
|
205
|
+
res = caps.lib.CAPSGetImageInfo(ct.byref(caps.pi), caps.iid)
|
|
206
|
+
error.check(res == 0,
|
|
207
|
+
f"{errprefix}: Could not get info for image '{name}'")
|
|
208
|
+
print(caps)
|
|
209
|
+
|
|
210
|
+
return caps
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class CTRaw(CAPS):
|
|
214
|
+
|
|
215
|
+
imagetype = 'CTRaw'
|
|
216
|
+
|
|
217
|
+
def __str__(self) -> str:
|
|
218
|
+
pi = self.pi
|
|
219
|
+
s = "CTRaw Image File:"
|
|
220
|
+
s += ("\n Cyls: %d-%d Heads: %d-%d"
|
|
221
|
+
% (pi.mincylinder, pi.maxcylinder, pi.minhead, pi.maxhead))
|
|
222
|
+
return s
|
|
223
|
+
|
|
224
|
+
def get_track(self, cyl: int, head: int) -> Optional[MasterTrack]:
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
ti = CAPSTrackInfo(self, cyl, head)
|
|
228
|
+
except CAPSTrackInfo.NoTrack:
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
# CTRaw dumps get bogus speed info from the CAPS library.
|
|
232
|
+
# Assume they are uniform density.
|
|
233
|
+
bit_ticks = None # ti.ticks
|
|
234
|
+
|
|
235
|
+
return MasterTrack(
|
|
236
|
+
bits = ti.bits,
|
|
237
|
+
time_per_rev = 60/ti.rpm,
|
|
238
|
+
bit_ticks = bit_ticks)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class IPFTrack(MasterTrack):
|
|
242
|
+
|
|
243
|
+
verify_revs: float = 2
|
|
244
|
+
tolerance = 100
|
|
245
|
+
|
|
246
|
+
sectors: RangeList
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def strong_data(sector: RangeList, weak: RangeList) -> Generator:
|
|
250
|
+
"""Return list of sector data areas excluding weak sections."""
|
|
251
|
+
def range_next(i):
|
|
252
|
+
s,l = next(i)
|
|
253
|
+
return s, s+l
|
|
254
|
+
weak_tol = 16 # Skip this number of bits after a weak area
|
|
255
|
+
weak_iter = it.chain(weak, [(1<<30,1)])
|
|
256
|
+
ws,we = -1,-1
|
|
257
|
+
sector_iter = iter(sector)
|
|
258
|
+
s,e = range_next(sector_iter)
|
|
259
|
+
try:
|
|
260
|
+
while True:
|
|
261
|
+
while we <= s:
|
|
262
|
+
ws,we = range_next(weak_iter)
|
|
263
|
+
we += weak_tol
|
|
264
|
+
if ws < e:
|
|
265
|
+
if s < ws:
|
|
266
|
+
yield (s,ws-s)
|
|
267
|
+
s = we
|
|
268
|
+
else:
|
|
269
|
+
yield (s,e-s)
|
|
270
|
+
s = e
|
|
271
|
+
if s >= e:
|
|
272
|
+
s,e = range_next(sector_iter)
|
|
273
|
+
except StopIteration:
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
def verify_track(self, flux: Flux) -> bool:
|
|
277
|
+
flux.cue_at_index()
|
|
278
|
+
raw = PLLTrack(clock = self.time_per_rev/len(self.bits), data = flux)
|
|
279
|
+
raw_bits, _ = raw.get_all_data()
|
|
280
|
+
for s,l in IPFTrack.strong_data(self.sectors, self.weak):
|
|
281
|
+
sector = self.bits[s:s+l]
|
|
282
|
+
# Search within an area +/- the pre-defined # bitcells tolerance
|
|
283
|
+
raw_area = raw_bits[max(self.splice + s - self.tolerance, 0)
|
|
284
|
+
: self.splice + s + l + self.tolerance]
|
|
285
|
+
# All we care about is at least one match (this is a bit fuzzy)
|
|
286
|
+
if next(raw_area.search(sector), None) is None:
|
|
287
|
+
return False
|
|
288
|
+
return True
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class IPF(CAPS):
|
|
292
|
+
|
|
293
|
+
imagetype = 'IPF'
|
|
294
|
+
|
|
295
|
+
def __str__(self) -> str:
|
|
296
|
+
pi = self.pi
|
|
297
|
+
s = "IPF Image File:"
|
|
298
|
+
if pi.release == 0x843265bb: # disk-utilities:IPF_ID
|
|
299
|
+
s += "\n SPS ID: None (https://github.com/keirf/disk-utilities)"
|
|
300
|
+
else:
|
|
301
|
+
s += "\n SPS ID: %04d (rev %d)" % (pi.release, pi.revision)
|
|
302
|
+
s += "\n Platform: "
|
|
303
|
+
nr_platforms = 0
|
|
304
|
+
for p in pi.platform:
|
|
305
|
+
if p == 0 and nr_platforms != 0:
|
|
306
|
+
break
|
|
307
|
+
if nr_platforms > 0:
|
|
308
|
+
s += ", "
|
|
309
|
+
s += pi.platform_name[p]
|
|
310
|
+
nr_platforms += 1
|
|
311
|
+
s += ("\n Created: %d/%d/%d %02d:%02d:%02d"
|
|
312
|
+
% (pi.crdt.year, pi.crdt.month, pi.crdt.day,
|
|
313
|
+
pi.crdt.hour, pi.crdt.min, pi.crdt.sec))
|
|
314
|
+
s += ("\n Cyls: %d-%d Heads: %d-%d"
|
|
315
|
+
% (pi.mincylinder, pi.maxcylinder, pi.minhead, pi.maxhead))
|
|
316
|
+
return s
|
|
317
|
+
|
|
318
|
+
def get_track(self, cyl: int, head: int) -> Optional[MasterTrack]:
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
ti = CAPSTrackInfo(self, cyl, head)
|
|
322
|
+
except CAPSTrackInfo.NoTrack:
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
data = []
|
|
326
|
+
for i in range(ti.sectorcnt):
|
|
327
|
+
si = CapsSectorInfo()
|
|
328
|
+
res = self.lib.CAPSGetInfo(ct.byref(si), self.iid,
|
|
329
|
+
cyl, head, 1, i)
|
|
330
|
+
error.check(res == 0, "Couldn't get sector info")
|
|
331
|
+
# Adjust the range start to be splice- rather than index-relative
|
|
332
|
+
data.append((si.datastart % ti.tracklen, si.datasize))
|
|
333
|
+
|
|
334
|
+
weak = []
|
|
335
|
+
for i in range(ti.weakcnt):
|
|
336
|
+
wi = CapsDataInfo()
|
|
337
|
+
res = self.lib.CAPSGetInfo(ct.byref(wi), self.iid,
|
|
338
|
+
cyl, head, 2, i)
|
|
339
|
+
error.check(res == 0, "Couldn't get weak data info")
|
|
340
|
+
# Adjust the range start to be splice- rather than index-relative
|
|
341
|
+
weak.append((wi.start % ti.tracklen, wi.size))
|
|
342
|
+
|
|
343
|
+
if ti.overlap < 0 and weak:
|
|
344
|
+
# Splice halfway through the longest weak area.
|
|
345
|
+
longest_weak = 0
|
|
346
|
+
for i,(s,n) in enumerate(weak):
|
|
347
|
+
if n > weak[longest_weak][1]:
|
|
348
|
+
longest_weak = i
|
|
349
|
+
s,n = weak[longest_weak]
|
|
350
|
+
if n > 200:
|
|
351
|
+
ti.overlap = (s + n//2) % ti.tracklen
|
|
352
|
+
|
|
353
|
+
if ti.overlap < 0 and data:
|
|
354
|
+
# Splice halfway through the longest gap area.
|
|
355
|
+
data.sort()
|
|
356
|
+
gap = []
|
|
357
|
+
for i,(s,n) in enumerate(data):
|
|
358
|
+
gap.append((data[(i+1)%len(data)][0] - s - n) % ti.tracklen)
|
|
359
|
+
i = gap.index(max(gap))
|
|
360
|
+
s,n = data[i]
|
|
361
|
+
ti.overlap = (s + n + gap[i] // 2) % ti.tracklen
|
|
362
|
+
|
|
363
|
+
if ti.overlap < 0:
|
|
364
|
+
# No sector or weak information. Splice at the index.
|
|
365
|
+
ti.overlap = 0
|
|
366
|
+
|
|
367
|
+
if ti.overlap:
|
|
368
|
+
|
|
369
|
+
# Adjust range starts to be splice- rather than index-relative
|
|
370
|
+
f = lambda x: ((x[0] - ti.overlap) % ti.tracklen, x[1])
|
|
371
|
+
data = list(map(f, data))
|
|
372
|
+
weak = list(map(f, weak))
|
|
373
|
+
|
|
374
|
+
# Rotate the track to start at the splice rather than the index.
|
|
375
|
+
ti.bits = ti.bits[ti.overlap:] + ti.bits[:ti.overlap]
|
|
376
|
+
if ti.ticks:
|
|
377
|
+
ti.ticks = ti.ticks[ti.overlap:] + ti.ticks[:ti.overlap]
|
|
378
|
+
|
|
379
|
+
# Sort ranges by start, and clip at splice point.
|
|
380
|
+
def clip_and_sort_ranges(r):
|
|
381
|
+
res = []
|
|
382
|
+
for i,(s,n) in enumerate(r):
|
|
383
|
+
if s + n > ti.tracklen:
|
|
384
|
+
res.append((s,ti.tracklen-s))
|
|
385
|
+
res.append((0, s+n-ti.tracklen))
|
|
386
|
+
else:
|
|
387
|
+
res.append((s,n))
|
|
388
|
+
res.sort()
|
|
389
|
+
return res
|
|
390
|
+
data = clip_and_sort_ranges(data)
|
|
391
|
+
weak = clip_and_sort_ranges(weak)
|
|
392
|
+
|
|
393
|
+
track = IPFTrack(
|
|
394
|
+
bits = ti.bits,
|
|
395
|
+
time_per_rev = 60/ti.rpm,
|
|
396
|
+
bit_ticks = ti.ticks,
|
|
397
|
+
splice = ti.overlap,
|
|
398
|
+
weak = weak
|
|
399
|
+
)
|
|
400
|
+
track.verify = track
|
|
401
|
+
track.sectors = data
|
|
402
|
+
return track
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# Open and initialise the CAPS library.
|
|
406
|
+
def open_libcaps():
|
|
407
|
+
|
|
408
|
+
# Get the OS-dependent list of valid CAPS library names.
|
|
409
|
+
_names = []
|
|
410
|
+
if platform.system() == "Linux":
|
|
411
|
+
_names = [ "libcapsimage.so.5", "libcapsimage.so.5.1",
|
|
412
|
+
"libcapsimage.so.4", "libcapsimage.so.4.2",
|
|
413
|
+
"libcapsimage.so" ]
|
|
414
|
+
elif platform.system() == "Darwin":
|
|
415
|
+
_names = [ "CAPSImage.framework/CAPSImage",
|
|
416
|
+
"CAPSImg.framework/CAPSImg" ]
|
|
417
|
+
elif platform.system() == "Windows":
|
|
418
|
+
_names = [ "CAPSImg_x64.dll", "CAPSImg.dll" ]
|
|
419
|
+
|
|
420
|
+
# Get the absolute path to the root Greaseweazle folder.
|
|
421
|
+
path = os.path.dirname(os.path.abspath(__file__))
|
|
422
|
+
for _ in range(3):
|
|
423
|
+
path = os.path.join(path, os.pardir)
|
|
424
|
+
path = os.path.normpath(path)
|
|
425
|
+
|
|
426
|
+
# Create a search list of both relative and absolute library names.
|
|
427
|
+
names = []
|
|
428
|
+
for name in _names:
|
|
429
|
+
names.append(name)
|
|
430
|
+
names.append(os.path.join(path, name))
|
|
431
|
+
if platform.system() == "Darwin":
|
|
432
|
+
names.append(os.path.join('/Library/Frameworks', name))
|
|
433
|
+
|
|
434
|
+
# Walk the search list, trying to open the CAPS library.
|
|
435
|
+
for name in names:
|
|
436
|
+
try:
|
|
437
|
+
lib = ct.cdll.LoadLibrary(name)
|
|
438
|
+
break
|
|
439
|
+
except:
|
|
440
|
+
pass
|
|
441
|
+
|
|
442
|
+
error.check("lib" in locals(), """\
|
|
443
|
+
Could not find SPS/CAPS library
|
|
444
|
+
For installation instructions please read the wiki:
|
|
445
|
+
<https://github.com/keirf/greaseweazle/wiki/IPF-Images>""")
|
|
446
|
+
|
|
447
|
+
# We have opened the library. Now initialise it.
|
|
448
|
+
res = lib.CAPSInit()
|
|
449
|
+
error.check(res == 0, "Failure initialising CAPS/SPS library '%s'" % name)
|
|
450
|
+
|
|
451
|
+
return lib
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# Get a reference to the CAPS library. Open it if necessary.
|
|
455
|
+
def get_libcaps():
|
|
456
|
+
global libcaps
|
|
457
|
+
if not 'libcaps' in globals():
|
|
458
|
+
libcaps = open_libcaps()
|
|
459
|
+
return libcaps
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# Local variables:
|
|
463
|
+
# python-indent: 4
|
|
464
|
+
# End:
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# greaseweazle/image/d64.py
|
|
2
|
+
#
|
|
3
|
+
# Written & released by Keir Fraser <keir.xen@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# This is free and unencumbered software released into the public domain.
|
|
6
|
+
# See the file COPYING for more details, or visit <http://unlicense.org>.
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import struct
|
|
11
|
+
|
|
12
|
+
from diskstack._vendor.greaseweazle import error
|
|
13
|
+
from diskstack._vendor.greaseweazle.image.image import Image
|
|
14
|
+
from diskstack._vendor.greaseweazle.image.img import IMG
|
|
15
|
+
from diskstack._vendor.greaseweazle.codec import codec
|
|
16
|
+
from diskstack._vendor.greaseweazle.codec.commodore import c64_gcr
|
|
17
|
+
|
|
18
|
+
class D64(IMG):
|
|
19
|
+
default_format = 'commodore.1541'
|
|
20
|
+
min_cyls: Optional[int] = 35
|
|
21
|
+
|
|
22
|
+
def get_disk_id(self):
|
|
23
|
+
t = self.get_track(17, 0) # BAM, track 18 (counting from 1), sector 0
|
|
24
|
+
if t is None:
|
|
25
|
+
return None
|
|
26
|
+
dat = t.get_img_track()
|
|
27
|
+
if len(dat) < 164:
|
|
28
|
+
return None
|
|
29
|
+
# NB disk_id byte order in BAM is reverse of sector headers
|
|
30
|
+
disk_id, = struct.unpack('<H', dat[162:164])
|
|
31
|
+
return disk_id
|
|
32
|
+
|
|
33
|
+
def from_bytes(self, dat: bytes) -> None:
|
|
34
|
+
super().from_bytes(dat)
|
|
35
|
+
disk_id = self.get_disk_id()
|
|
36
|
+
for _, t in self.to_track.items():
|
|
37
|
+
error.check(isinstance(t, c64_gcr.C64GCR),
|
|
38
|
+
f'{self.__class__.__name__}: '
|
|
39
|
+
f'Only {self.default_format} format is supported')
|
|
40
|
+
assert isinstance(t, c64_gcr.C64GCR) # mypy
|
|
41
|
+
if disk_id is not None:
|
|
42
|
+
t.set_disk_id(disk_id)
|
|
43
|
+
|
|
44
|
+
class D71(D64):
|
|
45
|
+
default_format = 'commodore.1571'
|
|
46
|
+
sequential = True
|
|
47
|
+
min_cyls = None
|
|
48
|
+
|
|
49
|
+
# Local variables:
|
|
50
|
+
# python-indent: 4
|
|
51
|
+
# End:
|