Created
December 14, 2021 07:24
-
-
Save bbbradsmith/db553489a35232447a8759f4364bb79f to your computer and use it in GitHub Desktop.
Pasti STX to ST conversion utility, for analyzing partial conversions from STX of damaged/copy-protected disks.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# | |
# Pasti .STX Atari ST image to .ST image converter. | |
# Creates an ST file using the sectors found in an STX, and reports missing/error sectors. | |
# | |
# Usage: | |
# stx_to_st("a.stx") | |
# stx_to_st("in.stx","out.st",tracks,sides,sectors) | |
# | |
# Based on Pasti reverse engineering: | |
# http://info-coach.fr/atari/documents/_mydoc/Pasti-documentation.pdf | |
# | |
# Brad Smith, 2021 | |
# http://rainwarrior.ca | |
# | |
import struct | |
# repeating pattern used to fill missing sectors | |
MISSING_SECTOR = bytes([0xDE,0xAD,0xD0,0x61,0x55,0x0D,0xEA,0xD0]*(512//8)) | |
#MISSING_SECTOR = bytes([0]*512) | |
def stx_to_st(filename,outfilename=None,tracks_override=None,sides_override=None,sectors_override=None): | |
sector = [] | |
# parse Pasti and poulate sector[] | |
print("Pasti: " + filename) | |
d = open(filename,"rb").read() | |
file_desc = struct.unpack("<BBBBHHHBBL",d[0:16]) | |
print("tracks: %d" % file_desc[7]) | |
p = 16 | |
for t in range(0,file_desc[7]): | |
#print("\nTrack Record %d at $%08X" % (t,p)) | |
track_desc = struct.unpack("<LLHHHBB",d[p:p+16]) | |
#print("fuzzy count: %d" % track_desc[1]) | |
#print("sectors: %d" % track_desc[2]) | |
track_number = track_desc[5] & 0x7F | |
track_side = track_desc[5] >> 7 | |
pnext = p + track_desc[0] | |
p += 16 | |
sector_descs = [] | |
timing_needed = False | |
if (track_desc[3] & 1): | |
for s in range(0,track_desc[2]): | |
#print("\nSector Descriptor %d:%d at $%08X" % (t,s,p)) | |
sector_desc = struct.unpack("<LHHBBBBHBB",d[p:p+16]) | |
sector_descs.append(sector_desc) | |
#print("offset: $%08X" % sector_desc[0]) | |
#print("flags: $%02X" % sector_desc[8]) | |
if sector_desc[8] & 1: | |
timing_needed = True | |
p+=16 | |
if (track_desc[1] > 0): | |
#print ("\nFuzzy Mask (%d bytes)" % track_desc[1]) | |
p += track_desc[1] | |
pimage = p | |
pimage_size = 0 | |
if (track_desc[3] & 0x40): # track image | |
#print ("\nTrack Image %d" % t) | |
ptrack_image = p | |
for sd in sector_descs: # collect sectors | |
o = ptrack_image+sd[0] # data offset | |
s = (track_number,track_side,sd[5], # track, side, sector | |
sd[8], # flags | |
d[o:o+512]) # data | |
sector.append(s) | |
if track_desc[3] & 0x80: | |
p += 2 | |
image_size = struct.unpack("<H",d[p:p+2])[0] | |
p += 2 | |
pimage_size = image_size | |
p += image_size | |
if (track_desc[3] & 1) == 0: | |
assert(False) # TODO haven't seen this data format yet | |
# presumably the track is 16 bytes + 512 * sectors, | |
# but not sure if 16 bytes is prefix or suffix? | |
# does 2 byte track length count? | |
for s in range(0,track_desc[2]): # track sector count | |
o = ptrack_image + 16 + (512 * s) | |
s = (track_number, stack_side, s+1, | |
0, # flags | |
d[o:o+512]) | |
sector.append(s) | |
if (track_desc[3] & 1): # sector descriptors = maybe sector images | |
sector_images = [] | |
if not (track_desc[3] & 0x40): # no track image = all sectors | |
sector_images = range(0,track_desc[2]) | |
else: | |
for s in range(0,track_desc[2]): | |
if sector_descs[s][0] >= image_size: # offset outside track image | |
sector_images.append(s) | |
for s in sector_images: | |
image_offset = sector_descs[s][0] | |
image_size = 128 << sector_descs[s][6] | |
p = pimage+image_offset+image_size # seek to end of this block? | |
if file_desc[8] == 2 and timing_needed and p < pnext: # timing record | |
#print ("\nTiming Data") | |
timing_desc = struct.unpack("<HH",d[p:p+4]) | |
p += timing_desc[1] | |
if p != pnext: | |
# track structures seem to be padded to even bytes ($FF for fill) | |
#print ("\nExtra Data? %d bytes" % (pnext-p)) | |
pass | |
p = pnext | |
if p != len(d): | |
#print("\nExtra Suffix Data? %d bytes" % (len(d)-p)) | |
pass | |
# | |
# Pasti parsed, collect sectors | |
# | |
if len(sector) < 0: | |
print("No sector data found?") | |
assert(False) | |
return | |
# find disk geometry | |
sector_min = 1 | |
sector_max = 9 | |
track_max = 79 | |
side_max = 0 | |
for (tnum,tside,snum,sflag,sdata) in sector: | |
#print("Sector: %02d.%01d:%02d %02X" % (tnum,tside,snum,sflag)) | |
sector_min = min(snum,sector_min) | |
sector_max = max(snum,sector_max) | |
track_max = max(tnum,track_max) | |
side_max = max(tside,side_max) | |
# override disk geometry | |
if sectors_override != None: sector_max = sectors_override | |
if tracks_override != None: track_max = tracks_override-1 | |
if sides_override != None: side_max = sides_override-1 | |
# build disk image and sector stats | |
side_stride = sector_max | |
track_stride = (side_max+1) * side_stride | |
sector_count = (track_max+1) * track_stride | |
sector_stat = [0] * sector_count | |
errors = False | |
st = bytearray(MISSING_SECTOR * sector_count) | |
for (tnum,tside,snum,sflag,sdata) in sector: | |
if tnum > track_max or tside > side_max or snum > sector_max: | |
continue | |
s = (tnum * track_stride) + (tside * side_stride) + (snum-1) | |
o = s * 512 | |
st[o:o+512] = sdata | |
# stats: | |
if sector_stat[s] != 0: sector_stat[s] |= 4 # bit 2 = duplicate | |
if sflag != 0: sector_stat[s] |= 2 # bit 1 = flag error | |
sector_stat[s] |= 1 # bit 0 = used | |
stat = "" | |
for s in range(sector_count): | |
if sector_stat[s] != 1: | |
errors = True | |
if s == 0: # header | |
stat += "TRAK" | |
for j in range(side_max+1): | |
stat += " " | |
for i in range(sector_max): | |
stat += "%X" % (i+1) | |
if (s % track_stride) == 0: # start of row | |
stat += "\n%02d:%1d" % ((s//track_stride),((s%track_stride)//side_stride)) | |
if (s % side_stride) == 0: # start of side | |
stat += " " | |
STAT = "x.?E?D?F" # x missing, . good, E error, D duplicate, F error+duplicate | |
stat += STAT[sector_stat[s]] | |
print(stat) | |
# save | |
if outfilename == None: | |
outfilename = filename | |
if outfilename.lower().endswith(".stx"): outfilename = outfilename[0:-4] | |
outfilename += ".st" | |
print("%2d tracks, %1d sides, %1d sectors" % (track_max+1,side_max+1,sector_max)) | |
print("Saving: " + outfilename) | |
open(outfilename,"wb").write(st) | |
if errors: | |
print("Done, with errors!") | |
else: | |
print("Done.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment