lib/DSK.rb in dsktool-0.2.1 vs lib/DSK.rb in dsktool-0.4.1

- old
+ new

@@ -1,102 +1,236 @@ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) require 'open-uri' + + # # For manipulating DSK files, as created by ADT (http://adt.berlios.de) and ADTPRo (http://adtpro.sourceforge.net) # used by many Apple 2 emulators. # class DSK + FILE_SYSTEMS=[:prodos,:dos33,:nadol,:pascal,:unknown,:none] + SECTOR_ORDERS=[:physical,:prodos_from_dos] + DSK_IMAGE_EXTENSIONS=[".dsk",".po",".do",".hdv"] + INTERLEAVES={ + :physical=> [0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F], + :prodos_from_dos=>[0x00,0x0E,0x0D,0x0C,0x0B,0x0A,0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x0F] + } + #does this filename have a suitable extension? def DSK.is_dsk_file?(filename) - !(filename.upcase=~/\.DSK$|\.DSK\.GZ$|\.PO$|\.PO\.GZ$/).nil? + extension=File.extname(File.basename(filename,".gz")).downcase + DSK_IMAGE_EXTENSIONS.include?(extension) end DSK_FILE_LENGTH=143360 - attr_accessor (:file_bytes) + attr_accessor :file_bytes,:sector_order,:track_count + + def file_system + :unknown + end + + +#add_file takes a *File object (of a type compatible with the underlying file system) and adds it to the in-memory image of this DSK +#this should be overridden in each *Disk type that has write support enabled + def add_file(new_file) + raise "add files to #{file_system} file system not yet supported" + end + +#make_file creates a *File object (of a type compatible with the underlying file system) from filename, contents and options required +#for the underlying file system. The file is NOT added to the in-memory image of this DSK (call add_file to do that) + def make_file(filename,contents,file_options={}) + raise "creating files for #{file_system} file system not yet supported" + end + + def delete_file(filename) + raise "deleting from #{file_system} file system not yet supported" + end + + def free_sector_list + raise "listing free sectors on #{file_system} file system not yet supported" + end + # does this DSK have a standard Apple DOS 3.3 VTOC? - def is_dos33? + def is_dos33?(sector_order) + #currently ignores sector order # VTOC is at offset 0x11000 - # bytes 1/2/3 are a track number, sector number and DOS version number + # bytes 1 & 2 are a track number, sector number and DOS version number + # byte 27 is maximum number of track/sector pairs which will fit in one file track/sector list sector (122 for 256 byte sectors) + # 35 number of sectors per track (16) # see if these are reasonable values - - (@file_bytes[0x11001]<=34) && (@file_bytes[0x11002]<=15) && (@file_bytes[0x11003]==3) + vtoc_sector=get_sector(0x11,0) + (vtoc_sector[01]<=34) && (vtoc_sector[02]<=15) && (vtoc_sector[0x27]==0x7A) && (vtoc_sector[0x35]==0x10) end - def is_nadol? + def is_nadol?(sector_order) + #currently ignores sector order # track $00, sector $02 , bytes $11 - "NADOL" (@file_bytes[0x00211..0x00215]=="NADOL") + end + + def is_prodos?(sector_order) + #block $02 - bytes $00/$01 are both $00, byte $04 is $F?, + #bytes $29-$2A = sectors ( 0x118 on a 35 track 5.25" disk) + first_sector_in_block_2=INTERLEAVES[sector_order][4] + first_sector_in_block_2=get_sector(0,4,sector_order) + (first_sector_in_block_2[0..1]=="\x00\x00") && (first_sector_in_block_2[4]>=0xF0) && (first_sector_in_block_2[0x29]+first_sector_in_block_2[0x2a]*0x100==track_count*8) end - + def is_pascal?(sector_order) + #block $02 - bytes $00..$01 are both $00, byte $06 is < 8 and + #bytes $0E-$0F = sectors ( 0x118 on a 35 track 5.25" disk) + first_sector_in_block_2=INTERLEAVES[sector_order][4] + first_sector_in_block_2=get_sector(0,4,sector_order) + (first_sector_in_block_2[0..1]=="\x00\x00") && (first_sector_in_block_2[6]<=7) && (first_sector_in_block_2[0x0E]+first_sector_in_block_2[0x0F]*0x100==track_count*8) + end + #create a new DSK structure (in memory, not on disk) - def initialize(file_bytes="\0"*DSK_FILE_LENGTH) - if (file_bytes.length!=DSK_FILE_LENGTH) then + def initialize(file_bytes="\0"*DSK_FILE_LENGTH,sector_order=:physical) + #file must be a multiple of (16 sectors * 256 bytes) = 4096 + #some dsks on Asimov appear to have an extra byte at the end so allow for 1 extra byte + if (file_bytes.length%4096>1) then raise "DSK files must be #{DSK_FILE_LENGTH} bytes long (was #{file_bytes.length} bytes)" end @file_bytes=file_bytes - @files={} + @files={} + @sector_order=sector_order + @track_count=file_bytes.length/4096 end + #write out DSK to file + def save_as(filename) + if !(filename=~/\.gz$/).nil? then + require 'zlib' + f=Zlib::GzipWriter.new(open(filename,"wb")) + else + f=open(filename,"wb") + end + f<<@file_bytes + f.close + end + + + #create a new DSK initialised with specified filesystem + def DSK.create_new(filesystem) + + filesystem=:dos33 if filesystem==:dos + + case filesystem + when :none + return DSK.new() + when :nadol + return DSK.read(File.dirname(__FILE__)+"/nadol_blank.po.gz") + when :dos33 + return DSK.read(File.dirname(__FILE__)+"/dos33_blank.dsk.gz") + else + raise "initialisation of #{filesystem} file system not currently supported" + end + end + + #read in an existing DSK file (must exist) def DSK.read(filename) #is the file extension .gz? if !(filename=~/\.gz$/).nil? then require 'zlib' file_bytes=Zlib::GzipReader.new(open(filename,"rb")).read else file_bytes=open(filename,"rb").read - end - if (file_bytes.length!=DSK_FILE_LENGTH) then - abort("#{filename} is not a valid DSK format file") - end - + end + + dsk=DSK.new(file_bytes) - if (dsk.is_dos33?) - require 'DOSDisk' - dsk=DOSDisk.new(file_bytes) - end - - if (dsk.is_nadol?) - require 'NADOLDisk' - dsk=NADOLDisk.new(file_bytes) - end + SECTOR_ORDERS.each do |sector_order| + begin + candidate_filesystem="unknown" + if (dsk.is_dos33?(sector_order)) + require 'DOSDisk' + candidate_filesystem="DOS 3.3" + dsk=DOSDisk.new(file_bytes,sector_order) + break + end + + if (dsk.is_nadol?(sector_order)) + require 'NADOLDisk' + candidate_filesystem="NADOL" + dsk=NADOLDisk.new(file_bytes,sector_order) + break + end + + if (dsk.is_prodos?(sector_order)) + require 'ProDOSDisk' + candidate_filesystem="ProDOS" + dsk=ProDOSDisk.new(file_bytes,sector_order) + break + end + + if (dsk.is_pascal?(sector_order)) + require 'PascalDisk' + candidate_filesystem="Pascal" + dsk=PascalDisk.new(file_bytes,sector_order) + break + end + rescue Exception=>e + STDERR<<"error while parsing #{filename} as #{candidate_filesystem} (sector order #{sector_order}\n" + STDERR<<"#{e}\n" + STDERR<<e.backtrace.join("\n") + end + end dsk end - def get_sector(track,sector) - start_byte=track*16*256+sector*256 + def get_sector(track,requested_sector,sector_order=@sector_order) + raise "bad sector #{requested_sector}" unless requested_sector.between?(0,0x0F) + raise "bad sector_order #{sector_order}" if INTERLEAVES[sector_order].nil? + physical_sector=INTERLEAVES[sector_order][requested_sector] + start_byte=track*16*256+physical_sector*256 @file_bytes[start_byte..start_byte+255] end + def set_sector(track,sector,contents) + physical_sector=INTERLEAVES[@sector_order][sector] + start_byte=track*16*256+physical_sector*256 + (0..255).each do |byte| + c=(contents[byte] || 0) + @file_bytes[start_byte+byte]=c + end +end + + #write supplied code to track 0 (from sector 0 to whatever is required) + #code should run from $0801 and can be up to 4KB (16 sectors) in length + def set_boot_track(contents) + sectors_needed=(contents.length / 256)+1 + raise "boot code can't exceed 16 sectors" if sectors_needed>16 + s=sectors_needed.chr+contents + for sector in 0..sectors_needed-1 + sector_data=s[sector*256..255+sector*256] + set_sector(0,sector,sector_data) + end + end + + def get_block(block_no) + track=(block_no / 8).to_i + first_sector=2*(block_no % 8) + raise "illegal block no #{block_no}" if track>=self.track_count + return self.get_sector(track,first_sector)+self.get_sector(track,first_sector+1) + end + def files @files end - #return a formatted hex dump of a single 256 byte sector + #return a formatted hex dump of a single 256 byte sector def dump_sector(track,sector) + require 'DumpUtilities' start_byte=track.to_i*16*256+sector.to_i*256 s=hline s<<sprintf("TRACK: $%02X SECTOR $%02X\ OFFSET $%04X\n",track,sector,start_byte) - s<< "\t" sector_data=get_sector(track,sector) - (0..15).each {|x| s<<sprintf("%02X ",x) } - s<<"\n" - s<<hline - (0..15).each {|line_number| - lhs="" - rhs="" - start_byte=line_number*16 - line=sector_data[start_byte..start_byte+15] - line.each_byte {|byte| - lhs<< sprintf("%02X ", byte) - rhs<< (byte%128).chr.sub(/[\x00-\x1f]/,'.') - } - s<<sprintf("%02X\t%s %s\n",start_byte,lhs,rhs) - } + s<< DumpUtilities.hex_dump(sector_data) s end #return a formatted hex dump of a single 256 byte sector def disassemble_sector(track,sector) @@ -108,11 +242,11 @@ return D65.disassemble(sector_data) end end #return a formatted hex dump of all sectors on all tracks - def dump_disk + def hex_dump s="" (0..34).each {|track| (0..15).each {|sector| s<<dump_sector(track,sector) } @@ -124,11 +258,31 @@ def hline "-"*79+"\n" end +end + +class DSKTrackSector + attr_accessor :track_no,:sector_no,:offset + def initialize(track_no,sector_no,offset=0x00) + @track_no=track_no + @sector_no=sector_no + @offset=offset + end + def to_s + sprintf "TRACK $%02X SECTOR $%02X OFFSET $02X",track_no,sector_no,offset + end + + def <=>(other) + return -1 unless other.kind_of?DSKTrackSector + return track_no<=>other.track_no unless track_no==other.track_no + return sector_no<=>other.sector_no unless sector_no==other.sector_no + return offset<=>other.offset + end end + # == Author # Jonno Downes (jonno@jamtronix.com) # # == Copyright # Copyright (c) 2007 Jonno Downes (jonno@jamtronix.com)