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)