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.
Files changed (119) hide show
  1. diskstack/__init__.py +5 -0
  2. diskstack/_vendor/__init__.py +0 -0
  3. diskstack/_vendor/greaseweazle/COPYING +24 -0
  4. diskstack/_vendor/greaseweazle/__init__.py +2 -0
  5. diskstack/_vendor/greaseweazle/codec/__init__.py +0 -0
  6. diskstack/_vendor/greaseweazle/codec/amiga/__init__.py +0 -0
  7. diskstack/_vendor/greaseweazle/codec/amiga/amigados.py +221 -0
  8. diskstack/_vendor/greaseweazle/codec/apple2/__init__.py +0 -0
  9. diskstack/_vendor/greaseweazle/codec/apple2/apple2_gcr.py +223 -0
  10. diskstack/_vendor/greaseweazle/codec/bitcell.py +117 -0
  11. diskstack/_vendor/greaseweazle/codec/codec.py +362 -0
  12. diskstack/_vendor/greaseweazle/codec/commodore/__init__.py +0 -0
  13. diskstack/_vendor/greaseweazle/codec/commodore/c64_gcr.py +229 -0
  14. diskstack/_vendor/greaseweazle/codec/datageneral/__init__.py +0 -0
  15. diskstack/_vendor/greaseweazle/codec/datageneral/datageneral.py +211 -0
  16. diskstack/_vendor/greaseweazle/codec/hp/__init__.py +0 -0
  17. diskstack/_vendor/greaseweazle/codec/hp/hp_mmfm.py +258 -0
  18. diskstack/_vendor/greaseweazle/codec/ibm/__init__.py +0 -0
  19. diskstack/_vendor/greaseweazle/codec/ibm/ibm.py +1005 -0
  20. diskstack/_vendor/greaseweazle/codec/macintosh/__init__.py +0 -0
  21. diskstack/_vendor/greaseweazle/codec/macintosh/mac_gcr.py +238 -0
  22. diskstack/_vendor/greaseweazle/codec/micropolis/__init__.py +0 -0
  23. diskstack/_vendor/greaseweazle/codec/micropolis/micropolis.py +233 -0
  24. diskstack/_vendor/greaseweazle/codec/northstar/__init__.py +0 -0
  25. diskstack/_vendor/greaseweazle/codec/northstar/northstar.py +210 -0
  26. diskstack/_vendor/greaseweazle/data/__init__.py +0 -0
  27. diskstack/_vendor/greaseweazle/data/diskdefs.cfg +81 -0
  28. diskstack/_vendor/greaseweazle/data/diskdefs_acorn.cfg +119 -0
  29. diskstack/_vendor/greaseweazle/data/diskdefs_akai.cfg +27 -0
  30. diskstack/_vendor/greaseweazle/data/diskdefs_amiga.cfg +17 -0
  31. diskstack/_vendor/greaseweazle/data/diskdefs_apple2.cfg +31 -0
  32. diskstack/_vendor/greaseweazle/data/diskdefs_apricot.cfg +25 -0
  33. diskstack/_vendor/greaseweazle/data/diskdefs_atari.cfg +33 -0
  34. diskstack/_vendor/greaseweazle/data/diskdefs_atarist.cfg +76 -0
  35. diskstack/_vendor/greaseweazle/data/diskdefs_coco.cfg +98 -0
  36. diskstack/_vendor/greaseweazle/data/diskdefs_commodore.cfg +127 -0
  37. diskstack/_vendor/greaseweazle/data/diskdefs_datageneral.cfg +8 -0
  38. diskstack/_vendor/greaseweazle/data/diskdefs_dec.cfg +37 -0
  39. diskstack/_vendor/greaseweazle/data/diskdefs_dragon.cfg +61 -0
  40. diskstack/_vendor/greaseweazle/data/diskdefs_eagle.cfg +25 -0
  41. diskstack/_vendor/greaseweazle/data/diskdefs_ensoniq.cfg +39 -0
  42. diskstack/_vendor/greaseweazle/data/diskdefs_epson.cfg +95 -0
  43. diskstack/_vendor/greaseweazle/data/diskdefs_gem.cfg +15 -0
  44. diskstack/_vendor/greaseweazle/data/diskdefs_hp.cfg +25 -0
  45. diskstack/_vendor/greaseweazle/data/diskdefs_ibm.cfg +140 -0
  46. diskstack/_vendor/greaseweazle/data/diskdefs_kaypro.cfg +58 -0
  47. diskstack/_vendor/greaseweazle/data/diskdefs_luxor.cfg +96 -0
  48. diskstack/_vendor/greaseweazle/data/diskdefs_mac.cfg +71 -0
  49. diskstack/_vendor/greaseweazle/data/diskdefs_micropolis.cfg +73 -0
  50. diskstack/_vendor/greaseweazle/data/diskdefs_mm1.cfg +69 -0
  51. diskstack/_vendor/greaseweazle/data/diskdefs_msx.cfg +45 -0
  52. diskstack/_vendor/greaseweazle/data/diskdefs_northstar.cfg +37 -0
  53. diskstack/_vendor/greaseweazle/data/diskdefs_occ1.cfg +23 -0
  54. diskstack/_vendor/greaseweazle/data/diskdefs_olivetti.cfg +18 -0
  55. diskstack/_vendor/greaseweazle/data/diskdefs_pc98.cfg +69 -0
  56. diskstack/_vendor/greaseweazle/data/diskdefs_raw.cfg +25 -0
  57. diskstack/_vendor/greaseweazle/data/diskdefs_rm.cfg +98 -0
  58. diskstack/_vendor/greaseweazle/data/diskdefs_sci.cfg +15 -0
  59. diskstack/_vendor/greaseweazle/data/diskdefs_sega.cfg +13 -0
  60. diskstack/_vendor/greaseweazle/data/diskdefs_sharp.cfg +13 -0
  61. diskstack/_vendor/greaseweazle/data/diskdefs_thomson.cfg +78 -0
  62. diskstack/_vendor/greaseweazle/data/diskdefs_tsc.cfg +81 -0
  63. diskstack/_vendor/greaseweazle/data/diskdefs_xerox.cfg +59 -0
  64. diskstack/_vendor/greaseweazle/data/diskdefs_zx.cfg +245 -0
  65. diskstack/_vendor/greaseweazle/error.py +19 -0
  66. diskstack/_vendor/greaseweazle/flux.py +315 -0
  67. diskstack/_vendor/greaseweazle/image/__init__.py +0 -0
  68. diskstack/_vendor/greaseweazle/image/a2r.py +133 -0
  69. diskstack/_vendor/greaseweazle/image/acorn.py +28 -0
  70. diskstack/_vendor/greaseweazle/image/adf.py +15 -0
  71. diskstack/_vendor/greaseweazle/image/apple2.py +18 -0
  72. diskstack/_vendor/greaseweazle/image/apridisk.py +107 -0
  73. diskstack/_vendor/greaseweazle/image/caps.py +464 -0
  74. diskstack/_vendor/greaseweazle/image/d64.py +51 -0
  75. diskstack/_vendor/greaseweazle/image/d81.py +28 -0
  76. diskstack/_vendor/greaseweazle/image/d88.py +211 -0
  77. diskstack/_vendor/greaseweazle/image/dcp.py +45 -0
  78. diskstack/_vendor/greaseweazle/image/dim.py +53 -0
  79. diskstack/_vendor/greaseweazle/image/dmk.py +166 -0
  80. diskstack/_vendor/greaseweazle/image/dsk.py +31 -0
  81. diskstack/_vendor/greaseweazle/image/edsk.py +591 -0
  82. diskstack/_vendor/greaseweazle/image/fd.py +23 -0
  83. diskstack/_vendor/greaseweazle/image/fdi.py +41 -0
  84. diskstack/_vendor/greaseweazle/image/hdm.py +15 -0
  85. diskstack/_vendor/greaseweazle/image/hfe.py +742 -0
  86. diskstack/_vendor/greaseweazle/image/image.py +130 -0
  87. diskstack/_vendor/greaseweazle/image/imd.py +201 -0
  88. diskstack/_vendor/greaseweazle/image/img.py +111 -0
  89. diskstack/_vendor/greaseweazle/image/kryoflux.py +304 -0
  90. diskstack/_vendor/greaseweazle/image/mgt.py +15 -0
  91. diskstack/_vendor/greaseweazle/image/msa.py +153 -0
  92. diskstack/_vendor/greaseweazle/image/nfd.py +113 -0
  93. diskstack/_vendor/greaseweazle/image/nsi.py +47 -0
  94. diskstack/_vendor/greaseweazle/image/scp.py +471 -0
  95. diskstack/_vendor/greaseweazle/image/sf7.py +15 -0
  96. diskstack/_vendor/greaseweazle/image/sharp2d.py +15 -0
  97. diskstack/_vendor/greaseweazle/image/td0.py +141 -0
  98. diskstack/_vendor/greaseweazle/image/xdf.py +15 -0
  99. diskstack/_vendor/greaseweazle/optimised/__init__.py +22 -0
  100. diskstack/_vendor/greaseweazle/tools/__init__.py +0 -0
  101. diskstack/_vendor/greaseweazle/tools/util.py +133 -0
  102. diskstack/_vendor/greaseweazle/track.py +494 -0
  103. diskstack/cache.py +210 -0
  104. diskstack/candidates.py +396 -0
  105. diskstack/cli.py +348 -0
  106. diskstack/errors.py +5 -0
  107. diskstack/filler.py +29 -0
  108. diskstack/formats.py +381 -0
  109. diskstack/parallel.py +73 -0
  110. diskstack/report.py +353 -0
  111. diskstack/stack.py +317 -0
  112. diskstack-1.0.0.dist-info/METADATA +329 -0
  113. diskstack-1.0.0.dist-info/RECORD +119 -0
  114. diskstack-1.0.0.dist-info/WHEEL +5 -0
  115. diskstack-1.0.0.dist-info/entry_points.txt +2 -0
  116. diskstack-1.0.0.dist-info/licenses/LICENSE +21 -0
  117. diskstack-1.0.0.dist-info/licenses/NOTICE +55 -0
  118. diskstack-1.0.0.dist-info/licenses/diskstack/_vendor/greaseweazle/COPYING +24 -0
  119. diskstack-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,742 @@
1
+ # greaseweazle/image/hfe.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, Dict, Tuple, Optional, List
10
+
11
+ import struct
12
+ import itertools as it
13
+
14
+ from diskstack._vendor.greaseweazle import error
15
+ from diskstack._vendor.greaseweazle.tools import util
16
+ from diskstack._vendor.greaseweazle.codec import codec
17
+ from diskstack._vendor.greaseweazle.codec.ibm import ibm
18
+ from diskstack._vendor.greaseweazle.codec.apple2 import apple2_gcr
19
+ from diskstack._vendor.greaseweazle.track import MasterTrack, PLLTrack
20
+ from bitarray import bitarray
21
+ from .image import Image, ImageOpts
22
+
23
+ InterfaceMode = {
24
+ 'IBMPC_DD': 0x00,
25
+ 'IBMPC_HD': 0x01,
26
+ 'ATARIST_DD': 0x02,
27
+ 'ATARIST_HD': 0x03,
28
+ 'AMIGA_DD': 0x04,
29
+ 'AMIGA_HD': 0x05,
30
+ 'CPC_DD': 0x06,
31
+ 'GENERIC_SHUGART_DD': 0x07,
32
+ 'IBMPC_ED': 0x08,
33
+ 'MSX2_DD': 0x09,
34
+ 'C64_DD': 0x0A,
35
+ 'EMU_SHUGART': 0x0B,
36
+ 'S950_DD': 0x0C,
37
+ 'S950_HD': 0x0D,
38
+ 'S950_DD_HD': 0x0E,
39
+ 'IBMPC_DD_HD': 0x0F,
40
+ 'QUICKDISK': 0x10,
41
+ 'UNKNOWN': 0xFF
42
+ }
43
+
44
+ EncodingType = {
45
+ 'ISOIBM_MFM': 0x00,
46
+ 'AMIGA_MFM': 0x01,
47
+ 'ISOIBM_FM': 0x02,
48
+ 'EMU_FM': 0x03,
49
+ 'TYCOM_FM': 0x04,
50
+ 'MEMBRAIN_MFM': 0x05,
51
+ 'APPLEII_GCR1': 0x06,
52
+ 'APPLEII_GCR2': 0x07,
53
+ 'APPLEII_HDDD_A2_GCR1': 0x08,
54
+ 'APPLEII_HDDD_A2_GCR2': 0x09,
55
+ 'ARBURGDAT': 0x0A,
56
+ 'ARBURGSYS': 0x0B,
57
+ 'AED6200P_MFM': 0x0C,
58
+ 'NORTHSTAR_HS_MFM': 0x0D,
59
+ 'HEATHKIT_HS_FM': 0x0E,
60
+ 'DEC_RX02_M2FM': 0x0F,
61
+ 'APPLEMAC_GCR': 0x10,
62
+ 'QD_MO5': 0x11,
63
+ 'C64_GCR': 0x12,
64
+ 'VICTOR9K_GCR': 0x13,
65
+ 'MICRALN_HS_FM': 0x14,
66
+ 'UNKNOWN': 0xFF
67
+ }
68
+
69
+ class HFEOpts(ImageOpts):
70
+ """bitrate: Bitrate of new HFE image file.
71
+ """
72
+
73
+ w_settings = [ 'bitrate', 'version', 'interface', 'encoding',
74
+ 'double_step', 'uniform' ]
75
+
76
+ def __init__(self) -> None:
77
+ self._bitrate: Optional[int] = None
78
+ self._version = 1
79
+ self._interface = 0xff
80
+ self._encoding = 0xff
81
+ self.double_step = False
82
+ self.uniform = False
83
+
84
+ @property
85
+ def bitrate(self) -> Optional[int]:
86
+ return self._bitrate
87
+ @bitrate.setter
88
+ def bitrate(self, bitrate: float):
89
+ try:
90
+ self._bitrate = int(bitrate)
91
+ if self._bitrate <= 0:
92
+ raise ValueError
93
+ except ValueError:
94
+ raise error.Fatal("HFE: Invalid bitrate: '%s'" % bitrate)
95
+
96
+ @property
97
+ def version(self) -> int:
98
+ return self._version
99
+ @version.setter
100
+ def version(self, version) -> None:
101
+ try:
102
+ self._version = int(version)
103
+ if self._version != 1 and self._version != 3:
104
+ raise ValueError
105
+ except ValueError:
106
+ raise error.Fatal("HFE: Invalid version: '%s'" % version)
107
+
108
+ @property
109
+ def interface(self):
110
+ return self._interface
111
+ @interface.setter
112
+ def interface(self, mode):
113
+ try:
114
+ self._interface = InterfaceMode[mode.upper()]
115
+ except KeyError:
116
+ try:
117
+ self._interface = int(mode, 0)
118
+ except ValueError:
119
+ l = [ x.lower() for x in InterfaceMode.keys() ]
120
+ l.sort()
121
+ raise error.Fatal("Bad HFE interface mode: '%s'\n" % mode
122
+ + 'Valid modes:\n' + util.columnify(l))
123
+
124
+ @property
125
+ def encoding(self):
126
+ return self._encoding
127
+ @encoding.setter
128
+ def encoding(self, enc):
129
+ try:
130
+ self._encoding = EncodingType[enc.upper()]
131
+ except KeyError:
132
+ try:
133
+ self._encoding = int(enc, 0)
134
+ except ValueError:
135
+ l = [ x.lower() for x in EncodingType.keys() ]
136
+ l.sort()
137
+ raise error.Fatal("Bad HFE encoding type: '%s'\n" % enc
138
+ + 'Valid types:\n' + util.columnify(l))
139
+
140
+ class HFETrack:
141
+ def __init__(self, track: MasterTrack) -> None:
142
+ self.track = track
143
+
144
+ @classmethod
145
+ def from_bitarray(cls, bits: bitarray, bitrate: int) -> HFETrack:
146
+ return cls(MasterTrack(
147
+ bits = bits, time_per_rev = len(bits) / (2000*bitrate)))
148
+
149
+ @classmethod
150
+ def from_hfe_bytes(cls, b: bytes, bitrate: int) -> HFETrack:
151
+ bits = bitarray(endian='big')
152
+ bits.frombytes(b)
153
+ bits.bytereverse()
154
+ return cls.from_bitarray(bits, bitrate)
155
+
156
+ def to_hfe_bytes(self) -> bytes:
157
+ bits = bitarray(endian='big')
158
+ bits.frombytes(self.track.bits.tobytes())
159
+ bits.bytereverse()
160
+ return bits.tobytes()
161
+
162
+
163
+ class HFE(Image):
164
+
165
+ opts: HFEOpts
166
+
167
+ def __init__(self, name: str, _fmt) -> None:
168
+ self.opts = HFEOpts()
169
+ self.filename = name
170
+ # Each track is (bitlen, rawbytes).
171
+ # rawbytes is a bytes() object in little-endian bit order.
172
+ self.to_track: Dict[Tuple[int,int], HFETrack] = dict()
173
+
174
+
175
+ def from_bytes(self, dat: bytes) -> None:
176
+
177
+ (sig, f_rev, n_cyl, n_side, t_enc, bitrate,
178
+ _, _, _, tlut_base) = struct.unpack("<8s4B2H2BH", dat[:20])
179
+ if sig == b"HXCHFEV3":
180
+ version = 3
181
+ else:
182
+ error.check(sig == b"HXCPICFE", "Not a valid HFE file")
183
+ version = 1
184
+ error.check(f_rev <= 1, "Not a valid HFE file")
185
+ error.check(0 < n_cyl, "HFE: Invalid #cyls")
186
+ error.check(0 < n_side < 3, "HFE: Invalid #sides")
187
+
188
+ self.opts.bitrate = bitrate
189
+ self.opts.version = version
190
+
191
+ tlut = dat[tlut_base*512:tlut_base*512+n_cyl*4]
192
+
193
+ for cyl in range(n_cyl):
194
+ for side in range(n_side):
195
+ offset, length = struct.unpack("<2H", tlut[cyl*4:(cyl+1)*4])
196
+ todo = length // 2
197
+ tdat = bytes()
198
+ while todo:
199
+ d_off = offset*512 + side*256
200
+ d_nr = 256 if todo > 256 else todo
201
+ tdat += dat[d_off:d_off+d_nr]
202
+ todo -= d_nr
203
+ offset += 1
204
+ track_v1 = HFETrack.from_hfe_bytes(tdat, bitrate)
205
+ if version == 1:
206
+ self.to_track[cyl,side] = track_v1
207
+ else:
208
+ self.to_track[cyl,side] = hfev3_mk_track(
209
+ cyl, side, track_v1)
210
+
211
+
212
+ def get_track(self, cyl: int, side: int) -> Optional[MasterTrack]:
213
+ if (cyl,side) not in self.to_track:
214
+ return None
215
+ return self.to_track[cyl,side].track
216
+
217
+
218
+ def emit_track(self, cyl: int, side: int, track) -> None:
219
+ # HFE convention is that FM and GCR are recorded at double rate
220
+ double_rate = (
221
+ (isinstance(track, ibm.IBMTrack) and track.mode is ibm.Mode.FM)
222
+ or isinstance(track, apple2_gcr.Apple2GCR))
223
+ t = track.master_track() if isinstance(track, codec.Codec) else track
224
+ if self.opts.bitrate is None:
225
+ error.check(hasattr(t, 'bitrate'),
226
+ 'HFE: Requires bitrate to be specified'
227
+ ' (eg. filename.hfe::bitrate=500)')
228
+ self.opts.bitrate = round(t.bitrate / 2e3)
229
+ assert self.opts.bitrate is not None # mypy
230
+ if double_rate:
231
+ self.opts.bitrate *= 2
232
+ print('HFE: Data bitrate detected: %d kbit/s' % self.opts.bitrate)
233
+ if isinstance(t, MasterTrack):
234
+ # Rotate data and timings to start at the index.
235
+ index = -t.splice % len(t.bits)
236
+ bits = t.bits[index:] + t.bits[:index]
237
+ bit_ticks = (t.bit_ticks[index:] + t.bit_ticks[:index]
238
+ if t.bit_ticks else None)
239
+ hardsector_bits = t.hardsector_bits
240
+ # Rotate the weak areas
241
+ weak = []
242
+ for s, n in t.weak:
243
+ s -= t.splice
244
+ if s < 0:
245
+ if s + n > 0:
246
+ weak.append((s % len(t.bits), -s))
247
+ weak.append((0, s + n))
248
+ else:
249
+ weak.append((s % len(t.bits), n))
250
+ else:
251
+ weak.append((s, n))
252
+ if double_rate:
253
+ double_bytes = ibm.doubler(bits.tobytes())
254
+ double_bits = bitarray(endian='big')
255
+ double_bits.frombytes(double_bytes)
256
+ bits = double_bits[:2*len(bits)]
257
+ if bit_ticks is not None:
258
+ bit_ticks = [x for x in bit_ticks for _ in range(2)]
259
+ weak = [(2*x,2*y) for (x,y) in weak]
260
+ if hardsector_bits is not None:
261
+ hardsector_bits = [2*x for x in hardsector_bits]
262
+ mt = MasterTrack(
263
+ bits = bits, time_per_rev = t.time_per_rev,
264
+ bit_ticks = bit_ticks, weak = weak,
265
+ hardsector_bits = hardsector_bits)
266
+ else:
267
+ flux = t.flux()
268
+ flux.cue_at_index()
269
+ raw = PLLTrack(clock = 5e-4 / self.opts.bitrate, data = flux)
270
+ bits, bit_ticks = raw.get_revolution(0)
271
+ if self.opts.uniform:
272
+ bit_ticks = None
273
+ mt = MasterTrack(
274
+ bits = bits, time_per_rev = flux.time_per_rev,
275
+ bit_ticks = bit_ticks,
276
+ hardsector_bits = raw.revolutions[0].hardsector_bits)
277
+ self.to_track[cyl,side] = HFETrack(mt)
278
+
279
+
280
+ def hfev1_get_image(self) -> bytes:
281
+
282
+ assert self.opts.version == 1
283
+ assert self.opts.bitrate is not None
284
+
285
+ n_side = 1
286
+ n_cyl = max(self.to_track.keys(), default=(0,), key=lambda x:x[0])[0]
287
+ n_cyl += 1
288
+
289
+ # We dynamically build the Track-LUT and -Data arrays.
290
+ tlut = bytearray()
291
+ tdat = bytearray()
292
+
293
+ # Stuff real data into the image.
294
+ for i in range(n_cyl):
295
+ s0 = self.to_track[i,0] if (i,0) in self.to_track else None
296
+ s1 = self.to_track[i,1] if (i,1) in self.to_track else None
297
+ if s0 is None and s1 is None:
298
+ # Dummy data for empty cylinders. Assumes 300RPM.
299
+ nr_bytes = 100 * self.opts.bitrate
300
+ tlut += struct.pack("<2H", len(tdat)//512 + 2, nr_bytes)
301
+ tdat += bytes([0x88] * (nr_bytes+0x1ff & ~0x1ff))
302
+ else:
303
+ # At least one side of this cylinder is populated.
304
+ if s1 is not None:
305
+ n_side = 2
306
+ bc = [s0.to_hfe_bytes() if s0 is not None else bytes(),
307
+ s1.to_hfe_bytes() if s1 is not None else bytes()]
308
+ nr_bytes = max(len(t) for t in bc)
309
+ nr_blocks = (nr_bytes + 0xff) // 0x100
310
+ tlut += struct.pack("<2H", len(tdat)//512 + 2, 2 * nr_bytes)
311
+ for b in range(nr_blocks):
312
+ for t in bc:
313
+ slice = t[b*256:(b+1)*256]
314
+ tdat += slice + bytes([0x88] * (256 - len(slice)))
315
+
316
+ # Construct the image header.
317
+ header = struct.pack("<8s4B2H2BH2B",
318
+ b"HXCPICFE",
319
+ 0,
320
+ n_cyl,
321
+ n_side,
322
+ self.opts.encoding,
323
+ self.opts.bitrate,
324
+ 0, # rpm (unused)
325
+ self.opts.interface,
326
+ 1, # rsvd
327
+ 1, # track list offset
328
+ 0xff, # write_allowed
329
+ 0xff if not self.opts.double_step else 0)
330
+
331
+ # Pad the header and TLUT to 512-byte blocks.
332
+ header += bytes([0xff] * (0x200 - len(header)))
333
+ tlut += bytes([0xff] * (0x200 - len(tlut)))
334
+
335
+ return header + tlut + tdat
336
+
337
+
338
+ def get_image(self) -> bytes:
339
+
340
+ # Empty disk may have no bitrate
341
+ if self.opts.bitrate is None:
342
+ assert not self.to_track
343
+ self.opts.bitrate = 250
344
+
345
+ if self.opts.version == 3:
346
+ return hfev3_get_image(self)
347
+
348
+ try:
349
+ return self.hfev1_get_image()
350
+ except struct.error as e:
351
+ raise error.Fatal(
352
+ '''\
353
+ HFE: Track too long to fit in image!
354
+ Are you trying to create an ED-rate image?
355
+ If so: You can't. Use another image format.''')
356
+
357
+
358
+ ###
359
+ ### HFE V3 TRACK PARSER
360
+ ###
361
+
362
+ class HFEv3_Op:
363
+ Nop = 0xf0
364
+ Index = 0xf1
365
+ Bitrate = 0xf2
366
+ SkipBits = 0xf3
367
+ Rand = 0xf4
368
+
369
+ class HFEv3_Range:
370
+ def __init__(self, s: int, n: int, val=0):
371
+ self.s, self.n, self.val = s, n, val
372
+ @property
373
+ def e(self) -> int:
374
+ return self.s + self.n
375
+
376
+ def hfev3_mk_track(cyl: int, head: int, track_v1: HFETrack) -> HFETrack:
377
+
378
+ def add_weak(weak: List[HFEv3_Range], pos: int, nr: int) -> None:
379
+ if len(weak) != 0:
380
+ w = weak[-1]
381
+ if w.e == pos:
382
+ w.n += nr
383
+ return
384
+ weak.append(HFEv3_Range(pos, nr))
385
+
386
+ # Input: Raw HFE
387
+ tdat = track_v1.track.bits.tobytes()
388
+
389
+ # Outputs: Bitcells, bitcell timings, weak areas.
390
+ bits = bitarray(endian='big')
391
+ ticks: List[int] = []
392
+ weak: List[HFEv3_Range] = []
393
+ index: List[int] = []
394
+
395
+ i = rate = 0
396
+ while i < len(tdat):
397
+ x, i = tdat[i], i+1
398
+ if x == HFEv3_Op.Nop:
399
+ pass
400
+ elif x == HFEv3_Op.Index:
401
+ index.append(len(bits))
402
+ elif x == HFEv3_Op.Bitrate:
403
+ if i+1 > len(tdat):
404
+ # Non fatal: This has been observed in HFEv3 images created
405
+ # by HxC tools (see issue #346).
406
+ print(f'T{cyl}.{head}: HFEv3: Truncated bitrate opcode')
407
+ break
408
+ rate, i = tdat[i], i+1
409
+ elif x == HFEv3_Op.SkipBits:
410
+ error.check(i+2 <= len(tdat),
411
+ f'T{cyl}.{head}: HFEv3: Truncated skipbits opcode')
412
+ nr, x, i = tdat[i], tdat[i+1], i+2
413
+ error.check(0 < nr < 8,
414
+ f'T{cyl}.{head}: HFEv3: Bad skipbits value: {nr}')
415
+ if x == HFEv3_Op.Rand:
416
+ add_weak(weak, len(bits), 8-nr)
417
+ x = 0
418
+ try:
419
+ bits.frombytes(bytes([x << nr]))
420
+ except ValueError:
421
+ raise error.Fatal(f'T{cyl}.{head}: HFEv3: Bad skipbits: '
422
+ f'0x{x:02x}<<{nr} = 0x{x<<nr:04x}')
423
+ bits = bits[:-nr]
424
+ ticks += [rate]*(8-nr)
425
+ elif x == HFEv3_Op.Rand:
426
+ add_weak(weak, len(bits), 8)
427
+ bits.frombytes(bytes(1))
428
+ ticks += [rate]*8
429
+ else:
430
+ error.check((x & 0xf0) != 0xf0,
431
+ f'T{cyl}.{head}: HFEv3: unrecognised opcode {x:02x}')
432
+ bits.frombytes(bytes([x]))
433
+ ticks += [rate]*8
434
+
435
+ # If the track did not start with a Bitrate opcode, cycle the rate round
436
+ # from the end of the track.
437
+ error.check(rate != 0, 'HFEv3: Bitrate was never set in track')
438
+ for i in range(len(ticks)):
439
+ if ticks[i] != 0: break
440
+ ticks[i] = rate
441
+
442
+ # This only works if the track is index aligned, with the first sector
443
+ # starting at bit 0.
444
+ hardsector_bits: Optional[List[int]] = None
445
+ if len(index) > 1:
446
+ pos = 0
447
+ hardsector_bits = []
448
+ for x in index[1:]:
449
+ hardsector_bits.append(x-pos)
450
+ pos = x
451
+ # The last index pulse is the pre-index hole: extend to end of track
452
+ hardsector_bits[-1] += len(bits) - pos
453
+
454
+ mt = MasterTrack(
455
+ bits = bits,
456
+ time_per_rev = sum(ticks)/36e6,
457
+ bit_ticks = cast(List[float], ticks), # mypy
458
+ weak = list(map(lambda x: (x.s, x.n), weak)),
459
+ hardsector_bits = hardsector_bits
460
+ )
461
+ return HFETrack(mt)
462
+
463
+
464
+ ###
465
+ ### HFE V3 TRACK GENERATOR
466
+ ###
467
+
468
+ # HFEv3_Chunk: Represents a consecutive range of input bitcells with identical
469
+ # bitcell timings and weakness property.
470
+ class HFEv3_Chunk:
471
+ def __init__(self, nbits: int, time_per_bit: float, is_random: bool,
472
+ emit_index: bool):
473
+ self.nbits = nbits
474
+ self.time_per_bit = time_per_bit
475
+ self.is_random = is_random
476
+ self.emit_index = emit_index
477
+ def __str__(self) -> str:
478
+ s = "%d bits, %.4fus per bit" % (self.nbits, self.time_per_bit*1e6)
479
+ if self.is_random:
480
+ s += ", random"
481
+ return s
482
+
483
+ class HFEv3_Generator:
484
+ def __init__(self, track: MasterTrack) -> None:
485
+ # Properties of the input track.
486
+ self.track = track
487
+ if track.bit_ticks is not None:
488
+ self.ticks_per_rev = sum(track.bit_ticks)
489
+ else:
490
+ self.ticks_per_rev = len(track.bits)
491
+ self.time_per_tick = track.time_per_rev / self.ticks_per_rev
492
+ # index_positions: Bit positions at which to insert index pulses
493
+ index_positions = track.hardsector_bits
494
+ if not index_positions:
495
+ index_positions = [ 0 ]
496
+ else:
497
+ index_positions[-1] //= 2
498
+ index_positions = list(it.accumulate([ 0 ] + index_positions))
499
+ index_positions.append(len(track.bits))
500
+ self.index_positions = index_positions
501
+ # tick_iter: An iterator over ranges of consecutive bitcells with
502
+ # identical ticks per bitcell.
503
+ self.ticks: List[Tuple[int,int,float]]
504
+ if track.bit_ticks is None:
505
+ self.ticks = [(0,len(track.bits),1)]
506
+ else:
507
+ self.ticks = []
508
+ c, s = track.bit_ticks[0], 0
509
+ for i, t in enumerate(track.bit_ticks):
510
+ if t != c:
511
+ self.ticks.append((s, i-s, c))
512
+ c, s = t, i
513
+ self.ticks.append((s, len(track.bit_ticks)-s, c))
514
+ self.tick_iter = map(lambda x: HFEv3_Range(*x),
515
+ it.chain(self.ticks, [(len(track.bits),0,0)]))
516
+ self.tick_cur = next(self.tick_iter)
517
+ # weak_iter: An iterator over weak ranges.
518
+ self.weak_iter = map(lambda x: HFEv3_Range(*x),
519
+ it.chain(track.weak, [(len(track.bits),0)]))
520
+ self.weak_cur = next(self.weak_iter)
521
+ # out: The raw HFEv3 output bytestream that we are generating.
522
+ self.out = bytearray()
523
+ # time: Input track current time, in seconds.
524
+ # hfe_time: HFE track output current time, in seconds.
525
+ self.time = self.hfe_time = 0.0
526
+ # pos: Input track current position, in bitcells.
527
+ self.pos = 0
528
+ # rate: Current HFE output rate, as set by HFEv3_Op.Bitrate.
529
+ # rate_change_pos: Byte position we last updated rate in HFE output.
530
+ self.rate, self.rate_change_pos = -1, 0
531
+ # sec: Which sector are we emitting
532
+ self.sec = 0
533
+ # chunk: Chunk of input bitcells currently being processed.
534
+ self.chunk = self.next_chunk()
535
+
536
+ def next_chunk(self) -> Optional[HFEv3_Chunk]:
537
+ # All done? Then return nothing.
538
+ if self.pos >= len(self.track.bits):
539
+ return None
540
+ # Position to next sector pulse
541
+ emit_index = False
542
+ while (n := self.index_positions[self.sec] - self.pos) <= 0:
543
+ assert n == 0 and not emit_index
544
+ self.sec += 1
545
+ emit_index = True
546
+ # Position among bitcells with identical timing.
547
+ while self.pos >= self.tick_cur.e:
548
+ self.tick_cur = next(self.tick_iter)
549
+ assert self.pos >= self.tick_cur.s
550
+ n = min(n, self.tick_cur.e - self.pos)
551
+ # Position relative to weak ranges.
552
+ while self.pos >= self.weak_cur.e:
553
+ self.weak_cur = next(self.weak_iter)
554
+ if self.pos < self.weak_cur.s:
555
+ n = min(n, self.weak_cur.s - self.pos)
556
+ is_random = False
557
+ else:
558
+ n = min(n, self.weak_cur.e - self.pos)
559
+ is_random = True
560
+ return HFEv3_Chunk(n, self.time_per_tick * self.tick_cur.val,
561
+ is_random, emit_index)
562
+
563
+ def increment_position(self, n: int) -> None:
564
+ c = self.chunk
565
+ assert c is not None
566
+ self.pos += n
567
+ self.time += n * c.time_per_bit
568
+ self.hfe_time += n * self.rate/36e6
569
+ c.nbits -= n
570
+ if c.nbits == 0:
571
+ self.chunk = self.next_chunk()
572
+
573
+ def raw_hfe_bytes(self) -> bytes:
574
+ x = bitarray(endian='big')
575
+ x.frombytes(self.out)
576
+ x.bytereverse()
577
+ return x.tobytes()
578
+
579
+ @classmethod
580
+ def empty(cls, time_per_rev: float, bitrate: float) -> HFEv3_Generator:
581
+ nbits = round(2000 * bitrate * time_per_rev)
582
+ return cls(MasterTrack(
583
+ bits = bitarray(nbits),
584
+ time_per_rev = time_per_rev,
585
+ weak = [(0, nbits)]))
586
+
587
+
588
+ def hfev3_get_image(hfe: HFE) -> bytes:
589
+
590
+ n_side = 1
591
+ n_cyl = max(hfe.to_track.keys(), default=(0,), key=lambda x:x[0])[0]
592
+ n_cyl += 1
593
+
594
+ # We dynamically build the Track-LUT and -Data arrays.
595
+ tlut = bytearray()
596
+ tdat = bytearray()
597
+
598
+ try:
599
+ default_time_per_rev = (next(iter(hfe.to_track.values()))
600
+ .track.time_per_rev)
601
+ except StopIteration:
602
+ default_time_per_rev = 0.2
603
+
604
+ bitrate = hfe.opts.bitrate
605
+ assert bitrate is not None
606
+
607
+ # Stuff real data into the image.
608
+ for cyl in range(n_cyl):
609
+ time_per_rev = (hfe.to_track[cyl,0].track.time_per_rev
610
+ if (cyl,0) in hfe.to_track
611
+ else hfe.to_track[cyl,1].track.time_per_rev
612
+ if (cyl,1) in hfe.to_track
613
+ else default_time_per_rev)
614
+ s = list()
615
+ s.append(HFEv3_Generator(hfe.to_track[cyl,0].track)
616
+ if (cyl,0) in hfe.to_track
617
+ else HFEv3_Generator.empty(time_per_rev, bitrate))
618
+ s.append(HFEv3_Generator(hfe.to_track[cyl,1].track)
619
+ if (cyl,1) in hfe.to_track
620
+ else HFEv3_Generator.empty(time_per_rev, bitrate))
621
+
622
+ if (cyl,1) in hfe.to_track:
623
+ n_side = 2
624
+
625
+ max_skew, max_delta = 0.0, 0.0
626
+ while True:
627
+ # Select the track 'x' with work to do and shortest output buffer.
628
+ x, y, c = s[0], s[1], s[0].chunk
629
+ if c is None or (len(y.out) < len(x.out) and y.chunk is not None):
630
+ x, y, c = y, x, y.chunk
631
+ if c is None:
632
+ break
633
+
634
+ # Calculate timing error across drive heads, in bitcells.
635
+ # This also accounts for differences in output byte position.
636
+ diff = (round((x.time - y.time) / c.time_per_bit)
637
+ + (len(y.out) - len(x.out)) * 8)
638
+ max_skew = max(max_skew, abs(diff))
639
+
640
+ # Adjust time per bit for accumulated error in time due to
641
+ # rounding error in the HFEv3_Op.Bitrate parameter.
642
+ # Note that we distribute the correction factor across the
643
+ # worst-case number of bitcells before we allow ourselves
644
+ # another minor bitrate adjustment.
645
+ rate_distance = 64 # byte-cells
646
+ tpb = c.time_per_bit + (x.time - x.hfe_time) / (rate_distance*8)
647
+
648
+ if c.emit_index:
649
+ c.emit_index = False
650
+ x.out.append(HFEv3_Op.Index)
651
+ diff -= 8
652
+
653
+ # Do a rate change if the rate has significantly changed or,
654
+ # for a change of +/-1, if we haven't changed rate in a while.
655
+ rate = round(tpb * 36e6)
656
+ if (rate != x.rate
657
+ and (abs(rate-x.rate) > 1 or diff >= 16
658
+ or (len(x.out) - x.rate_change_pos) > rate_distance)):
659
+ x.out.append(HFEv3_Op.Bitrate)
660
+ x.out.append(rate)
661
+ x.rate = rate
662
+ x.rate_change_pos = len(x.out)
663
+ diff -= 16
664
+
665
+ # Insert Nop padding if we still need it.
666
+ if diff >= 8:
667
+ x.out.append(HFEv3_Op.Nop)
668
+
669
+ # Emit up to 8 bitcells.
670
+ n = min(c.nbits, 8)
671
+ if n < 8:
672
+ x.out.append(HFEv3_Op.SkipBits)
673
+ x.out.append(8 - n)
674
+ if c.is_random:
675
+ x.out.append(HFEv3_Op.Rand)
676
+ else:
677
+ # Extract next bitcells into a stream-ready byte.
678
+ b = x.track.bits[x.pos:x.pos+n].tobytes()[0] >> (8-n)
679
+ # If the byte looks like an opcode, skip a bit.
680
+ if (b & 0xf0) == 0xf0:
681
+ n = 7
682
+ b >>= 1
683
+ x.out.append(HFEv3_Op.SkipBits)
684
+ x.out.append(8 - n)
685
+ # Emit the fixed-up byte.
686
+ x.out.append(b)
687
+
688
+ # Update tallies.
689
+ x.increment_position(n)
690
+ max_delta = max(max_delta, abs(x.time - x.hfe_time))
691
+
692
+ info = 'HFEv3 [%d] ' % cyl
693
+ for i,x in enumerate(s):
694
+ delta = len(x.out)-len(x.track.bits)//8
695
+ info += ('h%d:%d/+%d/+%.02f%% ' %
696
+ (i, len(x.out), delta, delta*8*100/len(x.track.bits)))
697
+ info += 'hskew:%dbc rate-err:%.02fus' % (max_skew, max_delta*1e6)
698
+ print(info)
699
+
700
+ nr_bytes = max(len(x.out) for x in s)
701
+ error.check(
702
+ nr_bytes < 32768,
703
+ '''\
704
+ HFEv3: Track too long to fit in image!
705
+ Are you trying to convert raw flux (SCP, KF, etc)?
706
+ If so: Try specifying 'uniform': eg. name.hfe::version=3:uniform
707
+ (will break variable-rate copy protections such as Copylock)
708
+ If not: Report a bug.''')
709
+
710
+ nr_blocks = (nr_bytes + 0xff) // 0x100
711
+ for x in s:
712
+ x.out += bytes([HFEv3_Op.Nop]) * (nr_blocks*0x100 - len(x.out))
713
+ bc = [x.raw_hfe_bytes() for x in s]
714
+ tlut += struct.pack("<2H", len(tdat)//512 + 2, 2 * nr_bytes)
715
+ for b in range(nr_blocks):
716
+ for t in bc:
717
+ tdat += t[b*256:(b+1)*256]
718
+
719
+ # Construct the image header.
720
+ header = struct.pack("<8s4B2H2BH2B",
721
+ b"HXCHFEV3",
722
+ 0,
723
+ n_cyl,
724
+ n_side,
725
+ hfe.opts.encoding,
726
+ hfe.opts.bitrate,
727
+ 0, # rpm (unused)
728
+ hfe.opts.interface,
729
+ 1, # rsvd
730
+ 1, # track list offset
731
+ 0xff, # write_allowed
732
+ 0xff if not hfe.opts.double_step else 0)
733
+
734
+ # Pad the header and TLUT to 512-byte blocks.
735
+ header += bytes([0xff] * (0x200 - len(header)))
736
+ tlut += bytes([0xff] * (0x200 - len(tlut)))
737
+
738
+ return header + tlut + tdat
739
+
740
+ # Local variables:
741
+ # python-indent: 4
742
+ # End: