edsk.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  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. import binascii, math, struct
  11. import itertools as it
  12. from bitarray import bitarray
  13. from greaseweazle import error
  14. from greaseweazle.codec.ibm import mfm
  15. from greaseweazle.track import MasterTrack, RawTrack
  16. from .image import Image
  17. class SR1:
  18. SUCCESS = 0x00
  19. CANNOT_FIND_ID_ADDRESS = 0x01
  20. WRITE_PROTECT_DETECTED = 0x02
  21. CANNOT_FIND_SECTOR_ID = 0x04
  22. RESERVED1 = 0x08
  23. OVERRUN = 0x10
  24. CRC_ERROR = 0x20
  25. RESERVED2 = 0x40
  26. END_OF_CYLINDER = 0x80
  27. class SR2:
  28. SUCCESS = 0x00
  29. MISSING_ADDRESS_MARK = 0x01
  30. BAD_CYLINDER = 0x02
  31. SCAN_COMMAND_FAILED = 0x04
  32. SCAN_COMMAND_EQUAL = 0x08
  33. WRONG_CYLINDER_DETECTED = 0x10
  34. CRC_ERROR_IN_SECTOR_DATA = 0x20
  35. SECTOR_WITH_DELETED_DATA = 0x40
  36. RESERVED = 0x80
  37. class SectorErrors:
  38. def __init__(self, sr1, sr2):
  39. self.id_crc_error = (sr1 & SR1.CRC_ERROR) != 0
  40. self.data_not_found = (sr2 & SR2.MISSING_ADDRESS_MARK) != 0
  41. self.data_crc_error = (sr2 & SR2.CRC_ERROR_IN_SECTOR_DATA) != 0
  42. self.deleted_dam = (sr2 & SR2.SECTOR_WITH_DELETED_DATA) != 0
  43. if self.data_crc_error:
  44. # uPD765 sets both id and data flags for data CRC errors
  45. self.id_crc_error = False
  46. if (# normal data
  47. (sr1 == SR1.SUCCESS and sr2 == SR2.SUCCESS) or
  48. # deleted data
  49. (sr1 == SR1.SUCCESS and sr2 == SR2.SECTOR_WITH_DELETED_DATA) or
  50. # end of track
  51. (sr1 == SR1.END_OF_CYLINDER and sr2 == SR2.SUCCESS) or
  52. # id crc error
  53. (sr1 == SR1.CRC_ERROR and sr2 == SR2.SUCCESS) or
  54. # normal data crc error
  55. (sr1 == SR1.CRC_ERROR and sr2 == SR2.CRC_ERROR_IN_SECTOR_DATA) or
  56. # deleted data crc error
  57. (sr1 == SR1.CRC_ERROR and sr2 == (SR2.CRC_ERROR_IN_SECTOR_DATA |
  58. SR2.SECTOR_WITH_DELETED_DATA)) or
  59. # data field missing (some FDCs set AM in ST1)
  60. (sr1 == SR1.CANNOT_FIND_ID_ADDRESS
  61. and sr2 == SR2.MISSING_ADDRESS_MARK) or
  62. # data field missing (some FDCs don't)
  63. (sr1 == SR1.SUCCESS and sr2 == SR2.MISSING_ADDRESS_MARK) or
  64. # CHRN mismatch
  65. (sr1 == SR1.CANNOT_FIND_SECTOR_ID and sr2 == SR2.SUCCESS) or
  66. # CHRN mismatch, including wrong cylinder
  67. (sr1 == SR1.CANNOT_FIND_SECTOR_ID
  68. and sr2 == SR2.WRONG_CYLINDER_DETECTED)):
  69. pass
  70. else:
  71. print('Unusual status flags (ST1=%02X ST2=%02X)' % (sr1, sr2))
  72. class EDSKTrack:
  73. gap_presync = 12
  74. gap_4a = 80 # Post-Index
  75. gap_1 = 50 # Post-IAM
  76. gap_2 = 22 # Post-IDAM
  77. gapbyte = 0x4e
  78. def __init__(self):
  79. self.time_per_rev = 0.2
  80. self.clock = 2e-6
  81. self.bits, self.weak, self.bytes = [], [], bytearray()
  82. def raw_track(self):
  83. track = MasterTrack(
  84. bits = self.bits,
  85. time_per_rev = self.time_per_rev,
  86. weak = self.weak)
  87. track.verify = self
  88. track.verify_revs = 1
  89. return track
  90. def _find_sync(self, bits, sync, start):
  91. for offs in bits.itersearch(sync):
  92. if offs >= start:
  93. return offs
  94. return None
  95. def verify_track(self, flux):
  96. flux.cue_at_index()
  97. raw = RawTrack(clock = self.clock, data = flux)
  98. bits, _ = raw.get_all_data()
  99. weak_iter = it.chain(self.weak, [(self.verify_len+1,1)])
  100. weak = next(weak_iter)
  101. # Start checking from the IAM sync
  102. dump_start = self._find_sync(bits, mfm.iam_sync, 0)
  103. self_start = self._find_sync(self.bits, mfm.iam_sync, 0)
  104. # Include the IAM pre-sync header
  105. if dump_start is None:
  106. return False
  107. dump_start -= self.gap_presync * 16
  108. self_start -= self.gap_presync * 16
  109. while self_start is not None and dump_start is not None:
  110. # Find the weak areas immediately before and after the current
  111. # region to be checked.
  112. s,n = None,None
  113. while self_start > weak[0]:
  114. s,n = weak
  115. weak = next(weak_iter)
  116. # If there is a weak area preceding us, move the start point to
  117. # immediately follow the weak area.
  118. if s is not None:
  119. delta = self_start - (s + n + 16)
  120. self_start -= delta
  121. dump_start -= delta
  122. # Truncate the region at the next weak area, or the last sector.
  123. self_end = max(self_start, min(weak[0], self.verify_len+1))
  124. dump_end = dump_start + self_end - self_start
  125. # Extract the corresponding areas from the pristine track and
  126. # from the dump, and check that they match.
  127. if bits[dump_start:dump_end] != self.bits[self_start:self_end]:
  128. return False
  129. # Find the next A1A1A1 sync pattern
  130. dump_start = self._find_sync(bits, mfm.sync, dump_end)
  131. self_start = self._find_sync(self.bits, mfm.sync, self_end)
  132. # Did we verify all regions in the pristine track?
  133. return self_start is None
  134. class EDSK(Image):
  135. read_only = True
  136. default_format = 'ibm.mfm'
  137. def __init__(self):
  138. self.to_track = dict()
  139. # Find all weak ranges in the given sector data copies.
  140. @staticmethod
  141. def find_weak_ranges(dat, size):
  142. orig = dat[:size]
  143. s, w = size, []
  144. # Find first mismatching byte across all copies
  145. for i in range(1, len(dat)//size):
  146. diff = [x^y for x, y in zip(orig, dat[size*i:size*(i+1)])]
  147. weak = [idx for idx, val in enumerate(diff) if val != 0]
  148. if weak:
  149. s = min(s, weak[0])
  150. # Look for runs of filler
  151. i = s
  152. while i < size:
  153. j, x = i, orig[i]
  154. while j < size and orig[j] == x:
  155. j += 1
  156. if j-i >= 16:
  157. w.append((s,i-s))
  158. s = j
  159. i = j
  160. # Append final weak area if any.
  161. if s < size:
  162. w.append((s,size-s))
  163. return w
  164. @staticmethod
  165. def _build_8k_track(sectors):
  166. if len(sectors) != 1:
  167. return None
  168. c,h,r,n,errs,data = sectors[0]
  169. if n != 6:
  170. return None
  171. if errs.id_crc_error or errs.data_not_found or not errs.data_crc_error:
  172. return None
  173. # Magic longtrack value is for Coin-Op Hits. Taken from SAMdisk.
  174. if len(data) > 6307:
  175. data = data[:6307]
  176. track = EDSKTrack()
  177. t = track.bytes
  178. # Post-index gap
  179. t += mfm.encode(bytes([track.gapbyte] * 16))
  180. # IAM
  181. t += mfm.encode(bytes(track.gap_presync))
  182. t += mfm.iam_sync_bytes
  183. t += mfm.encode(bytes([mfm.IBM_MFM.IAM]))
  184. t += mfm.encode(bytes([track.gapbyte] * 16))
  185. # IDAM
  186. t += mfm.encode(bytes(track.gap_presync))
  187. t += mfm.sync_bytes
  188. am = bytes([0xa1, 0xa1, 0xa1, mfm.IBM_MFM.IDAM, c, h, r, n])
  189. crc = mfm.crc16.new(am).crcValue
  190. am += struct.pack('>H', crc)
  191. t += mfm.encode(am[3:])
  192. t += mfm.encode(bytes([track.gapbyte] * track.gap_2))
  193. # DAM
  194. t += mfm.encode(bytes(track.gap_presync))
  195. t += mfm.sync_bytes
  196. dmark = (mfm.IBM_MFM.DDAM if errs.deleted_dam
  197. else mfm.IBM_MFM.DAM)
  198. am = bytes([0xa1, 0xa1, 0xa1, dmark]) + data
  199. t += mfm.encode(am[3:])
  200. return track
  201. @staticmethod
  202. def _build_kbi19_track(sectors):
  203. ids = [0,1,4,7,10,13,16,2,5,8,11,14,17,3,6,9,12,15,18]
  204. if len(sectors) != len(ids):
  205. return None
  206. for s,id in zip(sectors,ids):
  207. c,h,r,n,_,_ = s
  208. if r != id or n != 2:
  209. return None
  210. def addcrc(t,n):
  211. crc = mfm.crc16.new(mfm.decode(t[-n*2:])).crcValue
  212. t += mfm.encode(struct.pack('>H', crc))
  213. track = EDSKTrack()
  214. t = track.bytes
  215. # Post-index gap
  216. t += mfm.encode(bytes([track.gapbyte] * 64))
  217. # IAM
  218. t += mfm.encode(bytes(track.gap_presync))
  219. t += mfm.iam_sync_bytes
  220. t += mfm.encode(bytes([mfm.IBM_MFM.IAM]))
  221. t += mfm.encode(bytes([track.gapbyte] * 50))
  222. for idx, s in enumerate(sectors):
  223. c,h,r,n,errs,data = s
  224. # IDAM
  225. t += mfm.encode(bytes(track.gap_presync))
  226. t += mfm.sync_bytes
  227. t += mfm.encode(bytes([mfm.IBM_MFM.IDAM, c, h, r, n]))
  228. addcrc(t, 8)
  229. if r == 0:
  230. t += mfm.encode(bytes([track.gapbyte] * 17))
  231. t += mfm.encode(b' KBI ')
  232. else:
  233. t += mfm.encode(bytes([track.gapbyte] * 8))
  234. t += mfm.encode(b' KBI ')
  235. t += mfm.encode(bytes([track.gapbyte] * 9))
  236. # DAM
  237. t += mfm.encode(bytes(track.gap_presync))
  238. t += mfm.sync_bytes
  239. dmark = (mfm.IBM_MFM.DDAM if errs.deleted_dam
  240. else mfm.IBM_MFM.DAM)
  241. t += mfm.encode(bytes([dmark]))
  242. if idx%3 != 0:
  243. t += mfm.encode(data[:61])
  244. elif r == 0:
  245. t += mfm.encode(data[:512])
  246. addcrc(t,516)
  247. else:
  248. t += mfm.encode(data[0:0x10e])
  249. addcrc(t,516)
  250. t += mfm.encode(data[0x110:0x187])
  251. addcrc(t,516)
  252. t += mfm.encode(data[0x189:0x200])
  253. addcrc(t,516)
  254. t += mfm.encode(bytes([track.gapbyte] * 80))
  255. return track
  256. @classmethod
  257. def from_file(cls, name):
  258. with open(name, "rb") as f:
  259. dat = f.read()
  260. edsk = cls()
  261. sig, creator, ncyls, nsides, track_sz = struct.unpack(
  262. '<34s14s2BH', dat[:52])
  263. if sig[:8] == b'MV - CPC':
  264. extended = False
  265. elif sig[:16] == b'EXTENDED CPC DSK':
  266. extended = True
  267. else:
  268. raise error.Fatal('Unrecognised CPC DSK file: bad signature')
  269. if extended:
  270. track_sizes = list(dat[52:52+ncyls*nsides])
  271. track_sizes = list(map(lambda x: x*256, track_sizes))
  272. else:
  273. track_sizes = [track_sz] * (ncyls * nsides)
  274. o = 256 # skip disk header and track-size table
  275. for track_size in track_sizes:
  276. if track_size == 0:
  277. continue
  278. sig, cyl, head, sec_sz, nsecs, gap_3, filler = struct.unpack(
  279. '<12s4x2B2x4B', dat[o:o+24])
  280. error.check(sig == b'Track-Info\r\n',
  281. 'EDSK: Missing track header')
  282. error.check((cyl, head) not in edsk.to_track,
  283. 'EDSK: Track specified twice')
  284. bad_crc_clip_data = False
  285. while True:
  286. track = EDSKTrack()
  287. t = track.bytes
  288. # Post-index gap
  289. t += mfm.encode(bytes([track.gapbyte] * track.gap_4a))
  290. # IAM
  291. t += mfm.encode(bytes(track.gap_presync))
  292. t += mfm.iam_sync_bytes
  293. t += mfm.encode(bytes([mfm.IBM_MFM.IAM]))
  294. t += mfm.encode(bytes([track.gapbyte] * track.gap_1))
  295. sh = dat[o+24:o+24+8*nsecs]
  296. data_pos = o + 256 # skip track header and sector-info table
  297. clippable, ngap3, sectors, idam_included = 0, 0, [], False
  298. while sh:
  299. c, h, r, n, stat1, stat2, data_size = struct.unpack(
  300. '<6BH', sh[:8])
  301. sh = sh[8:]
  302. native_size = mfm.sec_sz(n)
  303. weak = []
  304. errs = SectorErrors(stat1, stat2)
  305. num_copies = 0 if errs.data_not_found else 1
  306. if not extended:
  307. data_size = mfm.sec_sz(sec_sz)
  308. sec_data = dat[data_pos:data_pos+data_size]
  309. data_pos += data_size
  310. if (extended
  311. and data_size > native_size
  312. and errs.data_crc_error
  313. and (data_size % native_size == 0
  314. or data_size == 49152)):
  315. num_copies = (3 if data_size == 49152
  316. else data_size // native_size)
  317. data_size //= num_copies
  318. weak = cls().find_weak_ranges(sec_data, data_size)
  319. sec_data = sec_data[:data_size]
  320. sectors.append((c,h,r,n,errs,sec_data))
  321. # IDAM
  322. if not idam_included:
  323. t += mfm.encode(bytes(track.gap_presync))
  324. t += mfm.sync_bytes
  325. am = bytes([0xa1, 0xa1, 0xa1, mfm.IBM_MFM.IDAM,
  326. c, h, r, n])
  327. crc = mfm.crc16.new(am).crcValue
  328. if errs.id_crc_error:
  329. crc ^= 0x5555
  330. am += struct.pack('>H', crc)
  331. t += mfm.encode(am[3:])
  332. t += mfm.encode(bytes([track.gapbyte] * track.gap_2))
  333. # DAM
  334. gap_included, idam_included = False, False
  335. if errs.id_crc_error or errs.data_not_found:
  336. continue
  337. t += mfm.encode(bytes(track.gap_presync))
  338. t += mfm.sync_bytes
  339. track.weak += [((s+len(t)//2+1)*16, n*16) for s,n in weak]
  340. dmark = (mfm.IBM_MFM.DDAM if errs.deleted_dam
  341. else mfm.IBM_MFM.DAM)
  342. if errs.data_crc_error:
  343. if sh:
  344. # Look for next IDAM
  345. idam = bytes([0]*12 + [0xa1]*3
  346. + [mfm.IBM_MFM.IDAM])
  347. idx = sec_data.find(idam)
  348. else:
  349. # Last sector: Look for GAP3
  350. idx = sec_data.find(bytes([track.gapbyte]*8))
  351. if idx > 0:
  352. # 2 + gap_3 = CRC + GAP3 (because gap_included)
  353. clippable += data_size - idx + 2 + gap_3
  354. if bad_crc_clip_data:
  355. data_size = idx
  356. sec_data = sec_data[:data_size]
  357. gap_included = True
  358. elif data_size < native_size:
  359. # Pad short data
  360. sec_data += bytes(native_size - data_size)
  361. elif data_size > native_size:
  362. # Clip long data if it includes pre-sync 00 bytes
  363. if (sec_data[-13] != 0
  364. and all([v==0 for v in sec_data[-12:]])):
  365. # Includes next pre-sync: Clip it.
  366. sec_data = sec_data[:-12]
  367. if sh:
  368. # Look for next IDAM
  369. idam = bytes([0]*12 + [0xa1]*3 + [mfm.IBM_MFM.IDAM]
  370. + list(sh[:4]))
  371. idx = sec_data.find(idam)
  372. if idx > native_size:
  373. # Sector data includes next IDAM. Output it
  374. # here and skip it on next iteration.
  375. t += mfm.encode(bytes([dmark]))
  376. t += mfm.encode(sec_data[:idx+12])
  377. t += mfm.sync_bytes
  378. t += mfm.encode(sec_data[idx+12+3:])
  379. idam_included = True
  380. continue
  381. # Long data includes CRC and GAP
  382. gap_included = True
  383. if gap_included:
  384. t += mfm.encode(bytes([dmark]))
  385. t += mfm.encode(sec_data)
  386. continue
  387. am = bytes([0xa1, 0xa1, 0xa1, dmark]) + sec_data
  388. crc = mfm.crc16.new(am).crcValue
  389. if errs.data_crc_error:
  390. crc ^= 0x5555
  391. am += struct.pack('>H', crc)
  392. t += mfm.encode(am[3:])
  393. if sh:
  394. # GAP3 for all but last sector
  395. t += mfm.encode(bytes([track.gapbyte] * gap_3))
  396. ngap3 += 1
  397. # Special track handlers
  398. special_track = cls()._build_8k_track(sectors)
  399. if special_track is None:
  400. special_track = cls()._build_kbi19_track(sectors)
  401. if special_track is not None:
  402. track = special_track
  403. break
  404. # The track may be too long to fit: Check for overhang.
  405. tracklen = int((track.time_per_rev / track.clock) / 16)
  406. overhang = int(len(t)//2 - tracklen*0.99)
  407. if overhang <= 0:
  408. break
  409. # Some EDSK tracks with Bad CRC contain a raw dump following
  410. # the DAM. This can usually be clipped.
  411. if clippable and not bad_crc_clip_data:
  412. bad_crc_clip_data = True
  413. continue
  414. # Some EDSK images have bogus GAP3 values. Shrink it if
  415. # necessary.
  416. new_gap_3 = -1
  417. if ngap3 != 0:
  418. new_gap_3 = gap_3 - math.ceil(overhang / ngap3)
  419. error.check(new_gap_3 >= 0,
  420. 'EDSK: Track %d.%d is too long '
  421. '(%d bits @ GAP3=%d; %d bits @ GAP3=0)'
  422. % (cyl, head, len(t)*8, gap_3,
  423. (len(t)//2-gap_3*ngap3)*16))
  424. #print('EDSK: GAP3 reduced (%d -> %d)' % (gap_3, new_gap_3))
  425. gap_3 = new_gap_3
  426. # Pre-index gap
  427. track.verify_len = len(track.bytes)*8
  428. tracklen = int((track.time_per_rev / track.clock) / 16)
  429. gap = max(40, tracklen - len(t)//2)
  430. track.bytes += mfm.encode(bytes([track.gapbyte] * gap))
  431. # Add the clock buts
  432. track.bits = bitarray(endian='big')
  433. track.bits.frombytes(mfm.mfm_encode(track.bytes))
  434. # Register the track
  435. edsk.to_track[cyl,head] = track
  436. o += track_size
  437. return edsk
  438. def get_track(self, cyl, side):
  439. if (cyl,side) not in self.to_track:
  440. return None
  441. return self.to_track[cyl,side].raw_track()
  442. # Local variables:
  443. # python-indent: 4
  444. # End: