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,591 @@
|
|
|
1
|
+
# greaseweazle/image/edsk.py
|
|
2
|
+
#
|
|
3
|
+
# Some of the code here is heavily inspired by Simon Owen's SAMdisk:
|
|
4
|
+
# https://simonowen.com/samdisk/
|
|
5
|
+
#
|
|
6
|
+
# Written & released by Keir Fraser <keir.xen@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
# This is free and unencumbered software released into the public domain.
|
|
9
|
+
# See the file COPYING for more details, or visit <http://unlicense.org>.
|
|
10
|
+
|
|
11
|
+
from typing import Dict, Tuple, Optional, List, Union
|
|
12
|
+
|
|
13
|
+
import binascii, math, struct
|
|
14
|
+
import itertools as it
|
|
15
|
+
from bitarray import bitarray
|
|
16
|
+
|
|
17
|
+
from diskstack._vendor.greaseweazle import __version__
|
|
18
|
+
from diskstack._vendor.greaseweazle import error
|
|
19
|
+
from diskstack._vendor.greaseweazle.flux import Flux
|
|
20
|
+
from diskstack._vendor.greaseweazle.codec.ibm import ibm
|
|
21
|
+
from diskstack._vendor.greaseweazle.track import MasterTrack, PLLTrack
|
|
22
|
+
from .image import Image
|
|
23
|
+
|
|
24
|
+
class EDSKRate:
|
|
25
|
+
Unknown = 0
|
|
26
|
+
DoubleDensity = 1
|
|
27
|
+
HighDensity = 2
|
|
28
|
+
ExtendedDensity = 3
|
|
29
|
+
|
|
30
|
+
class SR1:
|
|
31
|
+
SUCCESS = 0x00
|
|
32
|
+
CANNOT_FIND_ID_ADDRESS = 0x01
|
|
33
|
+
WRITE_PROTECT_DETECTED = 0x02
|
|
34
|
+
CANNOT_FIND_SECTOR_ID = 0x04
|
|
35
|
+
RESERVED1 = 0x08
|
|
36
|
+
OVERRUN = 0x10
|
|
37
|
+
CRC_ERROR = 0x20
|
|
38
|
+
RESERVED2 = 0x40
|
|
39
|
+
END_OF_CYLINDER = 0x80
|
|
40
|
+
|
|
41
|
+
class SR2:
|
|
42
|
+
SUCCESS = 0x00
|
|
43
|
+
MISSING_ADDRESS_MARK = 0x01
|
|
44
|
+
BAD_CYLINDER = 0x02
|
|
45
|
+
SCAN_COMMAND_FAILED = 0x04
|
|
46
|
+
SCAN_COMMAND_EQUAL = 0x08
|
|
47
|
+
WRONG_CYLINDER_DETECTED = 0x10
|
|
48
|
+
CRC_ERROR_IN_SECTOR_DATA = 0x20
|
|
49
|
+
SECTOR_WITH_DELETED_DATA = 0x40
|
|
50
|
+
RESERVED = 0x80
|
|
51
|
+
|
|
52
|
+
class SectorErrors:
|
|
53
|
+
def __init__(self, sr1: int, sr2: int):
|
|
54
|
+
self.id_crc_error = (sr1 & SR1.CRC_ERROR) != 0
|
|
55
|
+
self.data_not_found = (sr2 & SR2.MISSING_ADDRESS_MARK) != 0
|
|
56
|
+
self.data_crc_error = (sr2 & SR2.CRC_ERROR_IN_SECTOR_DATA) != 0
|
|
57
|
+
self.deleted_dam = (sr2 & SR2.SECTOR_WITH_DELETED_DATA) != 0
|
|
58
|
+
if self.data_crc_error:
|
|
59
|
+
# uPD765 sets both id and data flags for data CRC errors
|
|
60
|
+
self.id_crc_error = False
|
|
61
|
+
if (# normal data
|
|
62
|
+
(sr1 == SR1.SUCCESS and sr2 == SR2.SUCCESS) or
|
|
63
|
+
# deleted data
|
|
64
|
+
(sr1 == SR1.SUCCESS and sr2 == SR2.SECTOR_WITH_DELETED_DATA) or
|
|
65
|
+
# end of track
|
|
66
|
+
(sr1 == SR1.END_OF_CYLINDER and sr2 == SR2.SUCCESS) or
|
|
67
|
+
# id crc error
|
|
68
|
+
(sr1 == SR1.CRC_ERROR and sr2 == SR2.SUCCESS) or
|
|
69
|
+
# normal data crc error
|
|
70
|
+
(sr1 == SR1.CRC_ERROR and sr2 == SR2.CRC_ERROR_IN_SECTOR_DATA) or
|
|
71
|
+
# deleted data crc error
|
|
72
|
+
(sr1 == SR1.CRC_ERROR and sr2 == (SR2.CRC_ERROR_IN_SECTOR_DATA |
|
|
73
|
+
SR2.SECTOR_WITH_DELETED_DATA)) or
|
|
74
|
+
# data field missing (some FDCs set AM in ST1)
|
|
75
|
+
(sr1 == SR1.CANNOT_FIND_ID_ADDRESS
|
|
76
|
+
and sr2 == SR2.MISSING_ADDRESS_MARK) or
|
|
77
|
+
# data field missing (some FDCs don't)
|
|
78
|
+
(sr1 == SR1.SUCCESS and sr2 == SR2.MISSING_ADDRESS_MARK) or
|
|
79
|
+
# CHRN mismatch
|
|
80
|
+
(sr1 == SR1.CANNOT_FIND_SECTOR_ID and sr2 == SR2.SUCCESS) or
|
|
81
|
+
# CHRN mismatch, including wrong cylinder
|
|
82
|
+
(sr1 == SR1.CANNOT_FIND_SECTOR_ID
|
|
83
|
+
and sr2 == SR2.WRONG_CYLINDER_DETECTED)):
|
|
84
|
+
pass
|
|
85
|
+
else:
|
|
86
|
+
print('Unusual status flags (ST1=%02X ST2=%02X)' % (sr1, sr2))
|
|
87
|
+
|
|
88
|
+
class EDSKTrack:
|
|
89
|
+
|
|
90
|
+
gap_presync = 12
|
|
91
|
+
gap_4a = 80 # Post-Index
|
|
92
|
+
gap_1 = 50 # Post-IAM
|
|
93
|
+
gap_2 = 22 # Post-IDAM
|
|
94
|
+
|
|
95
|
+
gapbyte = 0x4e
|
|
96
|
+
|
|
97
|
+
verify_len: int
|
|
98
|
+
verify_revs: float = 1
|
|
99
|
+
|
|
100
|
+
def __init__(self, rate = 0) -> None:
|
|
101
|
+
self.time_per_rev = 0.2
|
|
102
|
+
if rate == EDSKRate.HighDensity:
|
|
103
|
+
self.clock = 1e-6
|
|
104
|
+
elif rate == EDSKRate.ExtendedDensity:
|
|
105
|
+
self.clock = 5e-7
|
|
106
|
+
else: # Double Density, or unknown
|
|
107
|
+
self.clock = 2e-6
|
|
108
|
+
self.bits = bitarray(endian='big')
|
|
109
|
+
self.weak: List[Tuple[int,int]] = []
|
|
110
|
+
self.bytes = bytearray()
|
|
111
|
+
|
|
112
|
+
def master_track(self) -> MasterTrack:
|
|
113
|
+
track = MasterTrack(
|
|
114
|
+
bits = self.bits,
|
|
115
|
+
time_per_rev = self.time_per_rev,
|
|
116
|
+
weak = self.weak)
|
|
117
|
+
track.verify = self
|
|
118
|
+
return track
|
|
119
|
+
|
|
120
|
+
def _find_sync(self, bits: bitarray, sync, start) -> Optional[int]:
|
|
121
|
+
for offs in bits.search(sync):
|
|
122
|
+
if offs >= start:
|
|
123
|
+
return offs
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
def verify_track(self, flux: Flux) -> bool:
|
|
127
|
+
flux.cue_at_index()
|
|
128
|
+
raw = PLLTrack(time_per_rev = self.time_per_rev,
|
|
129
|
+
clock = self.time_per_rev / len(self.bits),
|
|
130
|
+
data = flux)
|
|
131
|
+
bits, _ = raw.get_all_data()
|
|
132
|
+
weak_iter = it.chain(self.weak, [(self.verify_len+1,1)])
|
|
133
|
+
weak = next(weak_iter)
|
|
134
|
+
|
|
135
|
+
# Start checking from the IAM sync
|
|
136
|
+
dump_start = self._find_sync(bits, ibm.mfm_iam_sync, 0)
|
|
137
|
+
self_start = self._find_sync(self.bits, ibm.mfm_iam_sync, 0)
|
|
138
|
+
assert self_start is not None
|
|
139
|
+
|
|
140
|
+
# Include the IAM pre-sync header
|
|
141
|
+
if dump_start is None:
|
|
142
|
+
return False
|
|
143
|
+
dump_start -= self.gap_presync * 16
|
|
144
|
+
self_start -= self.gap_presync * 16
|
|
145
|
+
|
|
146
|
+
while self_start is not None and dump_start is not None:
|
|
147
|
+
|
|
148
|
+
# Find the weak areas immediately before and after the current
|
|
149
|
+
# region to be checked.
|
|
150
|
+
s,n = None,None
|
|
151
|
+
while self_start > weak[0]:
|
|
152
|
+
s,n = weak
|
|
153
|
+
weak = next(weak_iter)
|
|
154
|
+
|
|
155
|
+
# If there is a weak area preceding us, move the start point to
|
|
156
|
+
# immediately follow the weak area.
|
|
157
|
+
if s is not None:
|
|
158
|
+
assert n is not None # mypy
|
|
159
|
+
delta = self_start - (s + n + 16)
|
|
160
|
+
self_start -= delta
|
|
161
|
+
dump_start -= delta
|
|
162
|
+
|
|
163
|
+
# Truncate the region at the next weak area, or the last sector.
|
|
164
|
+
self_end = max(self_start, min(weak[0], self.verify_len+1))
|
|
165
|
+
dump_end = dump_start + self_end - self_start
|
|
166
|
+
|
|
167
|
+
# Extract the corresponding areas from the pristine track and
|
|
168
|
+
# from the dump, and check that they match.
|
|
169
|
+
if bits[dump_start:dump_end] != self.bits[self_start:self_end]:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
# Find the next A1A1A1 sync pattern
|
|
173
|
+
dump_start = self._find_sync(bits, ibm.mfm_sync, dump_end)
|
|
174
|
+
self_start = self._find_sync(self.bits, ibm.mfm_sync, self_end)
|
|
175
|
+
|
|
176
|
+
# Did we verify all regions in the pristine track?
|
|
177
|
+
return self_start is None
|
|
178
|
+
|
|
179
|
+
class EDSK(Image):
|
|
180
|
+
|
|
181
|
+
def __init__(self, name: str, _fmt) -> None:
|
|
182
|
+
self.to_track: Dict[Tuple[int,int],
|
|
183
|
+
Union[ibm.IBMTrack,EDSKTrack]] = dict()
|
|
184
|
+
self.filename = name
|
|
185
|
+
|
|
186
|
+
# Find all weak ranges in the given sector data copies.
|
|
187
|
+
@staticmethod
|
|
188
|
+
def find_weak_ranges(dat: bytes, size: int) -> List[Tuple[int,int]]:
|
|
189
|
+
orig = dat[:size]
|
|
190
|
+
s, w = size, []
|
|
191
|
+
# Find first mismatching byte across all copies
|
|
192
|
+
for i in range(1, len(dat)//size):
|
|
193
|
+
diff = [x^y for x, y in zip(orig, dat[size*i:size*(i+1)])]
|
|
194
|
+
weak = [idx for idx, val in enumerate(diff) if val != 0]
|
|
195
|
+
if weak:
|
|
196
|
+
s = min(s, weak[0])
|
|
197
|
+
# Look for runs of filler
|
|
198
|
+
i = s
|
|
199
|
+
while i < size:
|
|
200
|
+
j, x = i, orig[i]
|
|
201
|
+
while j < size and orig[j] == x:
|
|
202
|
+
j += 1
|
|
203
|
+
if j-i >= 16:
|
|
204
|
+
w.append((s,i-s))
|
|
205
|
+
s = j
|
|
206
|
+
i = j
|
|
207
|
+
# Append final weak area if any.
|
|
208
|
+
if s < size:
|
|
209
|
+
w.append((s,size-s))
|
|
210
|
+
return w
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _build_8k_track(sectors: List) -> Optional[EDSKTrack]:
|
|
214
|
+
if len(sectors) != 1:
|
|
215
|
+
return None
|
|
216
|
+
c,h,r,n,errs,data = sectors[0]
|
|
217
|
+
if n != 6:
|
|
218
|
+
return None
|
|
219
|
+
if errs.id_crc_error or errs.data_not_found or not errs.data_crc_error:
|
|
220
|
+
return None
|
|
221
|
+
# Magic longtrack value is for Coin-Op Hits. Taken from SAMdisk.
|
|
222
|
+
if len(data) > 6307:
|
|
223
|
+
data = data[:6307]
|
|
224
|
+
track = EDSKTrack()
|
|
225
|
+
t = track.bytes
|
|
226
|
+
# Post-index gap
|
|
227
|
+
t += ibm.encode(bytes([track.gapbyte] * 16))
|
|
228
|
+
# IAM
|
|
229
|
+
t += ibm.encode(bytes(track.gap_presync))
|
|
230
|
+
t += ibm.mfm_iam_sync_bytes
|
|
231
|
+
t += ibm.encode(bytes([ibm.Mark.IAM]))
|
|
232
|
+
t += ibm.encode(bytes([track.gapbyte] * 16))
|
|
233
|
+
# IDAM
|
|
234
|
+
t += ibm.encode(bytes(track.gap_presync))
|
|
235
|
+
t += ibm.mfm_sync_bytes
|
|
236
|
+
am = bytes([0xa1, 0xa1, 0xa1, ibm.Mark.IDAM, c, h, r, n])
|
|
237
|
+
crc = ibm.crc16.new(am).crcValue
|
|
238
|
+
am += struct.pack('>H', crc)
|
|
239
|
+
t += ibm.encode(am[3:])
|
|
240
|
+
t += ibm.encode(bytes([track.gapbyte] * track.gap_2))
|
|
241
|
+
# DAM
|
|
242
|
+
t += ibm.encode(bytes(track.gap_presync))
|
|
243
|
+
t += ibm.mfm_sync_bytes
|
|
244
|
+
dmark = (ibm.Mark.DDAM if errs.deleted_dam
|
|
245
|
+
else ibm.Mark.DAM)
|
|
246
|
+
am = bytes([0xa1, 0xa1, 0xa1, dmark]) + data
|
|
247
|
+
t += ibm.encode(am[3:])
|
|
248
|
+
return track
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def _build_kbi19_track(sectors: List) -> Optional[EDSKTrack]:
|
|
252
|
+
ids = [0,1,4,7,10,13,16,2,5,8,11,14,17,3,6,9,12,15,18]
|
|
253
|
+
if len(sectors) != len(ids):
|
|
254
|
+
return None
|
|
255
|
+
for s,id in zip(sectors,ids):
|
|
256
|
+
c,h,r,n,_,_ = s
|
|
257
|
+
if r != id or n != 2:
|
|
258
|
+
return None
|
|
259
|
+
def addcrc(t,n):
|
|
260
|
+
crc = ibm.crc16.new(ibm.decode(t[-n*2:])).crcValue
|
|
261
|
+
t += ibm.encode(struct.pack('>H', crc))
|
|
262
|
+
track = EDSKTrack()
|
|
263
|
+
t = track.bytes
|
|
264
|
+
# Post-index gap
|
|
265
|
+
t += ibm.encode(bytes([track.gapbyte] * 64))
|
|
266
|
+
# IAM
|
|
267
|
+
t += ibm.encode(bytes(track.gap_presync))
|
|
268
|
+
t += ibm.mfm_iam_sync_bytes
|
|
269
|
+
t += ibm.encode(bytes([ibm.Mark.IAM]))
|
|
270
|
+
t += ibm.encode(bytes([track.gapbyte] * 50))
|
|
271
|
+
for idx, s in enumerate(sectors):
|
|
272
|
+
c,h,r,n,errs,data = s
|
|
273
|
+
# IDAM
|
|
274
|
+
t += ibm.encode(bytes(track.gap_presync))
|
|
275
|
+
t += ibm.mfm_sync_bytes
|
|
276
|
+
t += ibm.encode(bytes([ibm.Mark.IDAM, c, h, r, n]))
|
|
277
|
+
addcrc(t, 8)
|
|
278
|
+
if r == 0:
|
|
279
|
+
t += ibm.encode(bytes([track.gapbyte] * 17))
|
|
280
|
+
t += ibm.encode(b' KBI ')
|
|
281
|
+
else:
|
|
282
|
+
t += ibm.encode(bytes([track.gapbyte] * 8))
|
|
283
|
+
t += ibm.encode(b' KBI ')
|
|
284
|
+
t += ibm.encode(bytes([track.gapbyte] * 9))
|
|
285
|
+
# DAM
|
|
286
|
+
t += ibm.encode(bytes(track.gap_presync))
|
|
287
|
+
t += ibm.mfm_sync_bytes
|
|
288
|
+
dmark = (ibm.Mark.DDAM if errs.deleted_dam
|
|
289
|
+
else ibm.Mark.DAM)
|
|
290
|
+
t += ibm.encode(bytes([dmark]))
|
|
291
|
+
if idx%3 != 0:
|
|
292
|
+
t += ibm.encode(data[:61])
|
|
293
|
+
elif r == 0:
|
|
294
|
+
t += ibm.encode(data[:512])
|
|
295
|
+
addcrc(t,516)
|
|
296
|
+
else:
|
|
297
|
+
t += ibm.encode(data[0:0x10e])
|
|
298
|
+
addcrc(t,516)
|
|
299
|
+
t += ibm.encode(data[0x110:0x187])
|
|
300
|
+
addcrc(t,516)
|
|
301
|
+
t += ibm.encode(data[0x189:0x200])
|
|
302
|
+
addcrc(t,516)
|
|
303
|
+
t += ibm.encode(bytes([track.gapbyte] * 80))
|
|
304
|
+
return track
|
|
305
|
+
|
|
306
|
+
def from_bytes(self, dat: bytes) -> None:
|
|
307
|
+
|
|
308
|
+
sig, creator, ncyls, nsides, track_sz = struct.unpack(
|
|
309
|
+
'<34s14s2BH', dat[:52])
|
|
310
|
+
if sig[:8] == b'MV - CPC':
|
|
311
|
+
extended = False
|
|
312
|
+
elif sig[:16] == b'EXTENDED CPC DSK':
|
|
313
|
+
extended = True
|
|
314
|
+
else:
|
|
315
|
+
raise error.Fatal('Unrecognised CPC DSK file: bad signature')
|
|
316
|
+
|
|
317
|
+
if extended:
|
|
318
|
+
track_sizes = list(dat[52:52+ncyls*nsides])
|
|
319
|
+
track_sizes = list(map(lambda x: x*256, track_sizes))
|
|
320
|
+
else:
|
|
321
|
+
track_sizes = [track_sz] * (ncyls * nsides)
|
|
322
|
+
|
|
323
|
+
o = 256 # skip disk header and track-size table
|
|
324
|
+
for track_size in track_sizes:
|
|
325
|
+
if track_size == 0:
|
|
326
|
+
continue
|
|
327
|
+
x = struct.unpack('<12s4x8B', dat[o:o+24])
|
|
328
|
+
sig, cyl, head, rate, mode, sec_sz, nsecs, gap_3, filler = x
|
|
329
|
+
error.check(sig == b'Track-Info\r\n',
|
|
330
|
+
'EDSK: Missing track header')
|
|
331
|
+
error.check((cyl, head) not in self.to_track,
|
|
332
|
+
'EDSK: Track specified twice')
|
|
333
|
+
bad_crc_clip_data = False
|
|
334
|
+
while True:
|
|
335
|
+
track = EDSKTrack(rate)
|
|
336
|
+
t = track.bytes
|
|
337
|
+
# Post-index gap
|
|
338
|
+
t += ibm.encode(bytes([track.gapbyte] * track.gap_4a))
|
|
339
|
+
# IAM
|
|
340
|
+
t += ibm.encode(bytes(track.gap_presync))
|
|
341
|
+
t += ibm.mfm_iam_sync_bytes
|
|
342
|
+
t += ibm.encode(bytes([ibm.Mark.IAM]))
|
|
343
|
+
t += ibm.encode(bytes([track.gapbyte] * track.gap_1))
|
|
344
|
+
sh = dat[o+24:o+24+8*nsecs]
|
|
345
|
+
data_pos = o + 256 # skip track header and sector-info table
|
|
346
|
+
clippable, ngap3, sectors, idam_included = 0, 0, [], False
|
|
347
|
+
while sh:
|
|
348
|
+
c, h, r, n, stat1, stat2, data_size = struct.unpack(
|
|
349
|
+
'<6BH', sh[:8])
|
|
350
|
+
sh = sh[8:]
|
|
351
|
+
native_size = ibm.sec_sz(n)
|
|
352
|
+
weak = []
|
|
353
|
+
errs = SectorErrors(stat1, stat2)
|
|
354
|
+
num_copies = 0 if errs.data_not_found else 1
|
|
355
|
+
if not extended:
|
|
356
|
+
data_size = ibm.sec_sz(sec_sz)
|
|
357
|
+
sec_data = dat[data_pos:data_pos+data_size]
|
|
358
|
+
data_pos += data_size
|
|
359
|
+
if (extended
|
|
360
|
+
and data_size > native_size
|
|
361
|
+
and errs.data_crc_error
|
|
362
|
+
and (data_size % native_size == 0
|
|
363
|
+
or data_size == 49152)):
|
|
364
|
+
num_copies = (3 if data_size == 49152
|
|
365
|
+
else data_size // native_size)
|
|
366
|
+
data_size //= num_copies
|
|
367
|
+
weak = EDSK.find_weak_ranges(sec_data, data_size)
|
|
368
|
+
sec_data = sec_data[:data_size]
|
|
369
|
+
sectors.append((c,h,r,n,errs,sec_data))
|
|
370
|
+
# IDAM
|
|
371
|
+
if not idam_included:
|
|
372
|
+
t += ibm.encode(bytes(track.gap_presync))
|
|
373
|
+
t += ibm.mfm_sync_bytes
|
|
374
|
+
am = bytes([0xa1, 0xa1, 0xa1, ibm.Mark.IDAM,
|
|
375
|
+
c, h, r, n])
|
|
376
|
+
crc = ibm.crc16.new(am).crcValue
|
|
377
|
+
if errs.id_crc_error:
|
|
378
|
+
crc ^= 0x5555
|
|
379
|
+
am += struct.pack('>H', crc)
|
|
380
|
+
t += ibm.encode(am[3:])
|
|
381
|
+
t += ibm.encode(bytes([track.gapbyte] * track.gap_2))
|
|
382
|
+
# DAM
|
|
383
|
+
gap_included, idam_included = False, False
|
|
384
|
+
if errs.id_crc_error or errs.data_not_found:
|
|
385
|
+
continue
|
|
386
|
+
t += ibm.encode(bytes(track.gap_presync))
|
|
387
|
+
t += ibm.mfm_sync_bytes
|
|
388
|
+
track.weak += [((s+len(t)//2+1)*16, n*16) for s,n in weak]
|
|
389
|
+
dmark = (ibm.Mark.DDAM if errs.deleted_dam
|
|
390
|
+
else ibm.Mark.DAM)
|
|
391
|
+
if errs.data_crc_error:
|
|
392
|
+
if sh:
|
|
393
|
+
# Look for next IDAM
|
|
394
|
+
idam = bytes([0]*12 + [0xa1]*3
|
|
395
|
+
+ [ibm.Mark.IDAM])
|
|
396
|
+
idx = sec_data.find(idam)
|
|
397
|
+
else:
|
|
398
|
+
# Last sector: Look for GAP3
|
|
399
|
+
idx = sec_data.find(bytes([track.gapbyte]*8))
|
|
400
|
+
if idx > 0:
|
|
401
|
+
# 2 + gap_3 = CRC + GAP3 (because gap_included)
|
|
402
|
+
clippable += data_size - idx + 2 + gap_3
|
|
403
|
+
if bad_crc_clip_data:
|
|
404
|
+
data_size = idx
|
|
405
|
+
sec_data = sec_data[:data_size]
|
|
406
|
+
gap_included = True
|
|
407
|
+
elif data_size < native_size:
|
|
408
|
+
# Pad short data
|
|
409
|
+
sec_data += bytes(native_size - data_size)
|
|
410
|
+
elif data_size > native_size:
|
|
411
|
+
# Clip long data if it includes pre-sync 00 bytes
|
|
412
|
+
if (sec_data[-13] != 0
|
|
413
|
+
and all([v==0 for v in sec_data[-12:]])):
|
|
414
|
+
# Includes next pre-sync: Clip it.
|
|
415
|
+
sec_data = sec_data[:-12]
|
|
416
|
+
if sh:
|
|
417
|
+
# Look for next IDAM
|
|
418
|
+
idam = bytes([0]*12 + [0xa1]*3 + [ibm.Mark.IDAM]
|
|
419
|
+
+ list(sh[:4]))
|
|
420
|
+
idx = sec_data.find(idam)
|
|
421
|
+
if idx > native_size:
|
|
422
|
+
# Sector data includes next IDAM. Output it
|
|
423
|
+
# here and skip it on next iteration.
|
|
424
|
+
t += ibm.encode(bytes([dmark]))
|
|
425
|
+
t += ibm.encode(sec_data[:idx+12])
|
|
426
|
+
t += ibm.mfm_sync_bytes
|
|
427
|
+
t += ibm.encode(sec_data[idx+12+3:])
|
|
428
|
+
idam_included = True
|
|
429
|
+
continue
|
|
430
|
+
# Long data includes CRC and GAP
|
|
431
|
+
gap_included = True
|
|
432
|
+
if gap_included:
|
|
433
|
+
t += ibm.encode(bytes([dmark]))
|
|
434
|
+
t += ibm.encode(sec_data)
|
|
435
|
+
continue
|
|
436
|
+
am = bytes([0xa1, 0xa1, 0xa1, dmark]) + sec_data
|
|
437
|
+
crc = ibm.crc16.new(am).crcValue
|
|
438
|
+
if errs.data_crc_error:
|
|
439
|
+
crc ^= 0x5555
|
|
440
|
+
am += struct.pack('>H', crc)
|
|
441
|
+
t += ibm.encode(am[3:])
|
|
442
|
+
if sh:
|
|
443
|
+
# GAP3 for all but last sector
|
|
444
|
+
t += ibm.encode(bytes([track.gapbyte] * gap_3))
|
|
445
|
+
ngap3 += 1
|
|
446
|
+
|
|
447
|
+
# Special track handlers
|
|
448
|
+
special_track = EDSK._build_8k_track(sectors)
|
|
449
|
+
if special_track is None:
|
|
450
|
+
special_track = EDSK._build_kbi19_track(sectors)
|
|
451
|
+
if special_track is not None:
|
|
452
|
+
track = special_track
|
|
453
|
+
break
|
|
454
|
+
|
|
455
|
+
# The track may be too long to fit: Check for overhang.
|
|
456
|
+
tracklen = int((track.time_per_rev / track.clock) / 16)
|
|
457
|
+
overhang = int(len(t)//2 - tracklen*0.99)
|
|
458
|
+
if overhang <= 0:
|
|
459
|
+
break
|
|
460
|
+
|
|
461
|
+
# Some EDSK tracks with Bad CRC contain a raw dump following
|
|
462
|
+
# the DAM. This can usually be clipped.
|
|
463
|
+
if clippable and not bad_crc_clip_data:
|
|
464
|
+
bad_crc_clip_data = True
|
|
465
|
+
continue
|
|
466
|
+
|
|
467
|
+
# Some EDSK images have bogus GAP3 values. Shrink it if
|
|
468
|
+
# necessary.
|
|
469
|
+
new_gap_3 = -1
|
|
470
|
+
if ngap3 != 0:
|
|
471
|
+
new_gap_3 = gap_3 - math.ceil(overhang / ngap3)
|
|
472
|
+
error.check(new_gap_3 >= 0,
|
|
473
|
+
'EDSK: Track %d.%d is too long '
|
|
474
|
+
'(%d bits @ GAP3=%d; %d bits @ GAP3=0)'
|
|
475
|
+
% (cyl, head, len(t)*8, gap_3,
|
|
476
|
+
(len(t)//2-gap_3*ngap3)*16))
|
|
477
|
+
#print('EDSK: GAP3 reduced (%d -> %d)' % (gap_3, new_gap_3))
|
|
478
|
+
gap_3 = new_gap_3
|
|
479
|
+
|
|
480
|
+
# Pre-index gap
|
|
481
|
+
track.verify_len = len(track.bytes)*8
|
|
482
|
+
tracklen = int((track.time_per_rev / track.clock) / 16)
|
|
483
|
+
gap = max(40, tracklen - len(t)//2)
|
|
484
|
+
track.bytes += ibm.encode(bytes([track.gapbyte] * gap))
|
|
485
|
+
|
|
486
|
+
# Add the clock buts
|
|
487
|
+
track.bits = bitarray(endian='big')
|
|
488
|
+
track.bits.frombytes(ibm.mfm_encode(track.bytes))
|
|
489
|
+
|
|
490
|
+
# Register the track
|
|
491
|
+
self.to_track[cyl,head] = track
|
|
492
|
+
o += track_size
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def get_track(self, cyl: int, side: int) -> Optional[MasterTrack]:
|
|
496
|
+
if (cyl,side) not in self.to_track:
|
|
497
|
+
return None
|
|
498
|
+
return self.to_track[cyl,side].master_track()
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def emit_track(self, cyl: int, side: int, track) -> None:
|
|
502
|
+
if isinstance(track, ibm.IBMTrack_Scan):
|
|
503
|
+
track = track.track
|
|
504
|
+
error.check(isinstance(track, ibm.IBMTrack),
|
|
505
|
+
'EDSK: Cannot create T%d.%d: Not IBM.FM nor IBM.MFM'
|
|
506
|
+
% (cyl, side))
|
|
507
|
+
if not isinstance(track, ibm.IBMTrack_Empty):
|
|
508
|
+
self.to_track[cyl,side] = track
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def get_image(self) -> bytes:
|
|
512
|
+
|
|
513
|
+
dat, tdat_list = bytearray(), list()
|
|
514
|
+
|
|
515
|
+
n_side = max(self.to_track.keys(), default=(0,0), key=lambda x:x[1])[1]
|
|
516
|
+
n_side += 1
|
|
517
|
+
|
|
518
|
+
n_cyl = max(self.to_track.keys(), default=(0,0), key=lambda x:x[0])[0]
|
|
519
|
+
n_cyl += 1
|
|
520
|
+
|
|
521
|
+
dat += struct.pack('<34s13sx2B2x',
|
|
522
|
+
b'EXTENDED CPC DSK File\r\nDisk-Info\r\n',
|
|
523
|
+
('GW %s' % __version__).encode(),
|
|
524
|
+
n_cyl, n_side)
|
|
525
|
+
|
|
526
|
+
for c in range(n_cyl):
|
|
527
|
+
|
|
528
|
+
for h in range(n_side):
|
|
529
|
+
|
|
530
|
+
# Empty track: NUL in Track Size Table
|
|
531
|
+
if (c,h) not in self.to_track:
|
|
532
|
+
dat += b'\x00'
|
|
533
|
+
continue
|
|
534
|
+
|
|
535
|
+
# TODO: Handle FM, and non-DD data rates
|
|
536
|
+
t = self.to_track[c,h]
|
|
537
|
+
assert isinstance(t, ibm.IBMTrack)
|
|
538
|
+
error.check(t.mode is ibm.Mode.MFM,
|
|
539
|
+
'EDSK: Cannot handle %s track format' % t.mode)
|
|
540
|
+
error.check(0.4e-6 < t.clock < 2.1e-6,
|
|
541
|
+
'EDSK: Cannot handle %.2fus clock' % (t.clock*1e6))
|
|
542
|
+
|
|
543
|
+
if t.clock < 0.75e-6:
|
|
544
|
+
rate = EDSKRate.ExtendedDensity
|
|
545
|
+
elif t.clock < 1.5e-6:
|
|
546
|
+
rate = EDSKRate.HighDensity
|
|
547
|
+
else:
|
|
548
|
+
# Double Density, but we'll just mark it as default (0)
|
|
549
|
+
rate = 0
|
|
550
|
+
|
|
551
|
+
# Estimate sec_sz from size of first sector only.
|
|
552
|
+
# Estimate gap3 from gap between first two sectors.
|
|
553
|
+
n_sec, sec_sz, gap3 = len(t.sectors), 0, 0
|
|
554
|
+
if n_sec > 0:
|
|
555
|
+
sec_sz = t.sectors[0].idam.n
|
|
556
|
+
gap3 = ibm.MFMGaps.gap3[t.sectors[0].idam.n]
|
|
557
|
+
if n_sec > 1:
|
|
558
|
+
gap3 = t.sectors[1].start - t.sectors[0].end
|
|
559
|
+
gap3 = max(0, round(gap3 / 16) - t.gap_presync)
|
|
560
|
+
tdat = struct.pack('<12s4x8B',
|
|
561
|
+
b'Track-Info\r\n', c, h, rate, 0,
|
|
562
|
+
sec_sz, n_sec, gap3, 0xe5)
|
|
563
|
+
|
|
564
|
+
# Track Information Block
|
|
565
|
+
for s in t.sectors:
|
|
566
|
+
tdat += struct.pack('<6BH',
|
|
567
|
+
s.idam.c, s.idam.h, s.idam.r, s.idam.n,
|
|
568
|
+
0, 0, ibm.sec_sz(s.idam.n))
|
|
569
|
+
tdat += bytes(-len(tdat) & 255)
|
|
570
|
+
|
|
571
|
+
# Track Data
|
|
572
|
+
for s in t.sectors:
|
|
573
|
+
tdat += s.dam.data[:]
|
|
574
|
+
|
|
575
|
+
# Track Size Table
|
|
576
|
+
assert (len(tdat) & 255) == 0
|
|
577
|
+
dat += struct.pack('B', len(tdat) // 256)
|
|
578
|
+
|
|
579
|
+
tdat_list.append(tdat)
|
|
580
|
+
|
|
581
|
+
# Pad and concatenate the whole image together.
|
|
582
|
+
dat += bytes(-len(dat) & 255)
|
|
583
|
+
for tdat in tdat_list:
|
|
584
|
+
dat += tdat
|
|
585
|
+
|
|
586
|
+
return dat
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# Local variables:
|
|
590
|
+
# python-indent: 4
|
|
591
|
+
# End:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# greaseweazle/image/fd.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
|
+
# FD is a widely used format in the Thomson MO/TO community, directly
|
|
9
|
+
# supported by emulators such as DCMOTO.
|
|
10
|
+
#
|
|
11
|
+
# The main difference between IMG and FD is that FD includes the two sides of
|
|
12
|
+
# a double-sided disk sequentially, rather than interleaved per-cylinder.
|
|
13
|
+
# That is, all tracks of side 0 are stored before all tracks of side 1.
|
|
14
|
+
|
|
15
|
+
from diskstack._vendor.greaseweazle.image.img import IMG
|
|
16
|
+
|
|
17
|
+
class FD(IMG):
|
|
18
|
+
default_format = 'thomson.1s320' # Most commonly used format
|
|
19
|
+
sequential = True # whole side 0 /then/ whole side 1
|
|
20
|
+
|
|
21
|
+
# Local variables:
|
|
22
|
+
# python-indent: 4
|
|
23
|
+
# End:
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# greaseweazle/image/fdi.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
|
+
import struct
|
|
9
|
+
|
|
10
|
+
from diskstack._vendor.greaseweazle import error
|
|
11
|
+
from diskstack._vendor.greaseweazle.image.img import IMG
|
|
12
|
+
from .image import Image
|
|
13
|
+
|
|
14
|
+
class FDI(IMG):
|
|
15
|
+
default_format = 'pc98.2hd'
|
|
16
|
+
read_only = True
|
|
17
|
+
|
|
18
|
+
def from_bytes(self, dat: bytes) -> None:
|
|
19
|
+
|
|
20
|
+
(magic, fdd_type, header_size, sector_size, sectors_per_track,
|
|
21
|
+
sides, tracks) = struct.unpack('<LLL4xLLLL', dat[:32])
|
|
22
|
+
error.check(magic == 0, 'FDI: Not a FDI file.')
|
|
23
|
+
error.check(fdd_type == 0x90, 'FDI: Unsupported format.')
|
|
24
|
+
error.check(sector_size == 1024, 'FDI: Unsupported sector size.')
|
|
25
|
+
error.check(sectors_per_track == 8,
|
|
26
|
+
'FDI: Unsupported number of sectors per track.')
|
|
27
|
+
error.check(sides == 2, 'FDI: Unsupported number of sides.')
|
|
28
|
+
error.check(tracks == 77, 'FDI: Unsupported number of tracks.')
|
|
29
|
+
|
|
30
|
+
pos = header_size
|
|
31
|
+
for cyl, head in self.track_list():
|
|
32
|
+
if self.sides_swapped:
|
|
33
|
+
head ^= 1
|
|
34
|
+
track = self.fmt.mk_track(cyl, head)
|
|
35
|
+
if track is not None:
|
|
36
|
+
pos += track.set_img_track(dat[pos:])
|
|
37
|
+
self.to_track[cyl,head] = track
|
|
38
|
+
|
|
39
|
+
# Local variables:
|
|
40
|
+
# python-indent: 4
|
|
41
|
+
# End:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# greaseweazle/image/hdm.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 HDM(IMG):
|
|
11
|
+
default_format = 'pc98.2hd'
|
|
12
|
+
|
|
13
|
+
# Local variables:
|
|
14
|
+
# python-indent: 4
|
|
15
|
+
# End:
|