Skip to content

Instantly share code, notes, and snippets.

@bbbradsmith
Created December 14, 2021 07:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bbbradsmith/db553489a35232447a8759f4364bb79f to your computer and use it in GitHub Desktop.
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.
#!/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