Class: DSK

Inherits:
Object
  • Object
show all
Defined in:
lib/DSK.rb

Overview

For manipulating DSK files, as created by ADT (adt.berlios.de) and ADTPRo (adtpro.sourceforge.net) used by many Apple 2 emulators.

Direct Known Subclasses

CPMDisk, DOSDisk, NADOLDisk, PascalDisk, ProDOSDisk

Constant Summary collapse

FILE_SYSTEMS =
[:prodos,:dos33,:nadol,:cpm,:pascal,:modified_dos,:unknown,:none]
SECTOR_ORDERS =
[:physical,:dos]
DSK_IMAGE_EXTENSIONS =
[".dsk",".po",".do",".hdv",".nib"]
INTERLEAVES =
{
	:physical=>   [0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F],
	:dos=>[0x00,0x0E,0x0D,0x0C,0x0B,0x0A,0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x0F],
}
DSK_FILE_LENGTH =
143360
NIB_FILE_LENGTH =
232960

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file_bytes = "\0"*DSK_FILE_LENGTH, sector_order = :physical) ⇒ DSK

create a new DSK structure (in memory, not on disk)



116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/DSK.rb', line 116

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={}
	@sector_order=sector_order
	@track_count=file_bytes.length/4096
   @source_filename="(unknown)"
end

Instance Attribute Details

#file_bytesObject

Returns the value of attribute file_bytes.



28
29
30
# File 'lib/DSK.rb', line 28

def file_bytes
  @file_bytes
end

#sector_orderObject

Returns the value of attribute sector_order.



28
29
30
# File 'lib/DSK.rb', line 28

def sector_order
  @sector_order
end

#source_filenameObject

Returns the value of attribute source_filename.



28
29
30
# File 'lib/DSK.rb', line 28

def source_filename
  @source_filename
end

#track_countObject

Returns the value of attribute track_count.



28
29
30
# File 'lib/DSK.rb', line 28

def track_count
  @track_count
end

Class Method Details

.create_new(filesystem) ⇒ Object

create a new DSK initialised with specified filesystem



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/DSK.rb', line 143

def DSK.create_new(filesystem)
   
  filesystem=:dos33 if filesystem==:dos 
  
  case filesystem 
    when :none
      return DSK.new()
   when :cpm
      require 'CPMDisk'
      return CPMDisk.new("\xe5"*DSK_FILE_LENGTH,:physical)
    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

.is_dsk_file?(filename) ⇒ Boolean

does this filename have a suitable extension?

Returns:

  • (Boolean)


22
23
24
25
# File 'lib/DSK.rb', line 22

def DSK.is_dsk_file?(filename)
	extension=File.extname(File.basename(filename,".gz")).downcase
	DSK_IMAGE_EXTENSIONS.include?(extension)
end

.read(filename) ⇒ Object

read in an existing DSK file (must exist)



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/DSK.rb', line 215

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-NIB_FILE_LENGTH).abs<=1 then
     require 'Nibbles'
     dsk=Nibbles.make_dsk_from_nibbles(file_bytes)
   else
     dsk=DSK.new(file_bytes)
   end
   dsk.source_filename=filename		
	dsk.best_subclass
end

Instance Method Details

#add_file(new_file) ⇒ Object

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



37
38
39
# File 'lib/DSK.rb', line 37

def add_file(new_file)
  raise "add files to #{file_system} file system not yet supported"
end

#best_subclassObject

for a generic DSK,return an instance of subclass representing the the best match file system



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/DSK.rb', line 163

def best_subclass
  SECTOR_ORDERS.each do |sector_order|
	begin
		candidate_filesystem="unknown"
		if (self.is_dos33?(sector_order)) 
			require 'DOSDisk'
			candidate_filesystem="DOS 3.3"
			return DOSDisk.new(self.file_bytes,sector_order)
		end
		
		if (self.is_nadol?(sector_order)) 
			require 'NADOLDisk'
			candidate_filesystem="NADOL"
			return NADOLDisk.new(self.file_bytes,sector_order)
		end
		
		if (self.is_prodos?(sector_order))
			require 'ProDOSDisk'
			candidate_filesystem="ProDOS"
			return ProDOSDisk.new(self.file_bytes,sector_order)
		end
     
		if (self.is_pascal?(sector_order))
			require 'PascalDisk'
			candidate_filesystem="Pascal"
			return PascalDisk.new(self.file_bytes,sector_order)
		end
      
      if (self.is_cpm?(sector_order))
			require 'CPMDisk'
			candidate_filesystem="CP/M"
			return CPMDisk.new(self.file_bytes,sector_order)
		end
	rescue Exception=>e
		STDERR<<"error while parsing #{self.source_filename} as #{candidate_filesystem} (sector order #{sector_order}\n"
		STDERR<<"#{e}\n"
		STDERR<<e.backtrace.join("\n")
	end      
end
  
  #if none of the above matched, look for a DOS image with a VTOC in the wrong spot
  0.upto(0x22) do |track|
    if (self.is_modified_dos?(track,0)) 
			require 'DOSDisk'
			candidate_filesystem="MODIFIED DOS"
			return DOSDisk.new(self.file_bytes,:physical,track,0)
		end
  end
  #if we didn't find a better match, return self
  self
end

#delete_file(filename) ⇒ Object



47
48
49
# File 'lib/DSK.rb', line 47

def delete_file(filename)
  raise "deleting from #{file_system} file system not yet supported"
end

#disassemble_sector(track, sector) ⇒ Object

return a formatted hex dump of a single 256 byte sector



286
287
288
289
290
291
292
293
294
# File 'lib/DSK.rb', line 286

def disassemble_sector(track,sector)
	require 'D65'
	sector_data=get_sector(track,sector)
	if (track==0) && (sector==0) then
		return D65.disassemble(sector_data[1..255],0x801)
	else
		return D65.disassemble(sector_data)
	end
end

#dump_sector(track, sector) ⇒ Object

return a formatted hex dump of a single 256 byte sector



275
276
277
278
279
280
281
282
283
# File 'lib/DSK.rb', line 275

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)
	sector_data=get_sector(track,sector)
	s<< DumpUtilities.hex_dump(sector_data)
	s
end

#file_systemObject



30
31
32
# File 'lib/DSK.rb', line 30

def file_system
	:unknown
end

#filesObject



270
271
272
# File 'lib/DSK.rb', line 270

def files
	@files
end

#free_sector_listObject



51
52
53
# File 'lib/DSK.rb', line 51

def free_sector_list
  raise "listing free sectors on #{file_system} file system not yet supported"
end

#get_block(block_no) ⇒ Object



263
264
265
266
267
268
# File 'lib/DSK.rb', line 263

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

#get_sector(track, requested_sector, sector_order = @sector_order) ⇒ Object



234
235
236
237
238
239
240
# File 'lib/DSK.rb', line 234

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,256]
end

#hex_dumpObject

return a formatted hex dump of all sectors on all tracks



297
298
299
300
301
302
303
304
305
# File 'lib/DSK.rb', line 297

def hex_dump
	s=""
	(0..34).each {|track|
		(0..15).each {|sector|
			s<<dump_sector(track,sector)
		}
	}
	s
end

#is_cpm?(sector_order) ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/DSK.rb', line 78

def is_cpm?(sector_order) 
  #currently ignores sector order
  #look for a valid looking CPM directory on track 3.
  #go through each sector in order, for each sector, look at every 32nd byte, and see if it is a valid 'user number' (i.e. a number from 00..0F). Stop looking when you see an 'E5'.
  #if an invalid user number is found before an E5, then the disk is NOT a CPM disk
  found_0xE5_byte=false
  [0x0,0x6,0xC,0x3,0x9,0xF,0xE,0x5].each do |sector_no|
    sector=get_sector(3,INTERLEAVES[sector_order][sector_no])
    [0x00,0x20,0x40,0x60,0x80,0xA0,0xC0,0xE0].each do |byte_number|
      if (sector[byte_number]>0x0F && sector[byte_number]!=0xe5 && sector[byte_number]!=0x1F) then
  #      puts "found #{sprintf '%02x',sector[byte_number]} at #{sprintf '%02x', byte_number} sector #{sprintf '%02x', sector_no}"
        return false 
      end
      if (sector[byte_number]==0xe5) then
        found_0xE5_byte=true
      end
    end
  end
  return found_0xE5_byte #if we've only seen 00 bytes, then it's not really a CPM 
end

#is_dos33?(sector_order) ⇒ Boolean

does this DSK have a standard Apple DOS 3.3 VTOC?

Returns:

  • (Boolean)


56
57
58
59
60
61
62
63
64
65
# File 'lib/DSK.rb', line 56

def	is_dos33?(sector_order)
	#currently ignores sector order
	# VTOC is at offset 0x11000
	# 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
   vtoc_sector=get_sector(0x11,0)
	(vtoc_sector[01]<=34) && (vtoc_sector[02]<=15) && (vtoc_sector[0x27]==0x7A) && (vtoc_sector[0x35]==0x10)
end

#is_modified_dos?(vtoc_track_no, vtoc_sector_no) ⇒ Boolean

Returns:

  • (Boolean)


67
68
69
70
# File 'lib/DSK.rb', line 67

def is_modified_dos?(vtoc_track_no,vtoc_sector_no)
  vtoc_sector=get_sector(vtoc_track_no,vtoc_sector_no)
(vtoc_sector[01]<=34) && (vtoc_sector[02]<=15) && (vtoc_sector[0x27]==0x7A) && (vtoc_sector[0x35]==0x10)
end

#is_nadol?(sector_order) ⇒ Boolean

Returns:

  • (Boolean)


72
73
74
75
76
# File 'lib/DSK.rb', line 72

def	is_nadol?(sector_order)
	#currently ignores sector order
	# track $00, sector $02 , bytes $11 - "NADOL"
	(@file_bytes[0x00211..0x00215]=="NADOL")
end

#is_pascal?(sector_order) ⇒ Boolean

Returns:

  • (Boolean)


107
108
109
110
111
112
113
# File 'lib/DSK.rb', line 107

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

#is_prodos?(sector_order) ⇒ Boolean

Returns:

  • (Boolean)


99
100
101
102
103
104
105
# File 'lib/DSK.rb', line 99

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

#make_file(filename, contents, file_options = {}) ⇒ Object

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)



43
44
45
# File 'lib/DSK.rb', line 43

def make_file(filename,contents,file_options={})
  raise "creating files for #{file_system} file system not yet supported"
end

#save_as(filename) ⇒ Object

write out DSK to file



130
131
132
133
134
135
136
137
138
139
# File 'lib/DSK.rb', line 130

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

#set_boot_track(contents) ⇒ Object

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



253
254
255
256
257
258
259
260
261
# File 'lib/DSK.rb', line 253

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,256]
    set_sector(0,sector,sector_data)
  end
end

#set_sector(track, sector, contents) ⇒ Object



242
243
244
245
246
247
248
249
# File 'lib/DSK.rb', line 242

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