File Coverage

blib/lib/AppleII/Disk.pm
Criterion Covered Total %
statement 139 144 96.5
branch 46 68 67.6
condition 14 17 82.3
subroutine 30 31 96.7
pod 6 6 100.0
total 235 266 88.3


line stmt bran cond sub pod time code
1             #---------------------------------------------------------------------
2             package AppleII::Disk;
3             #
4             # Copyright 1996-2006 Christopher J. Madsen
5             #
6             # Author: Christopher J. Madsen
7             # Created: 25 Jul 1996
8             #
9             # This program is free software; you can redistribute it and/or modify
10             # it under the same terms as Perl itself.
11             #
12             # This program is distributed in the hope that it will be useful,
13             # but WITHOUT ANY WARRANTY; without even the implied warranty of
14             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either the
15             # GNU General Public License or the Artistic License for more details.
16             #
17             # ABSTRACT: Block-level access to Apple II disk image files
18             #---------------------------------------------------------------------
19              
20 3     3   29548 use 5.006;
  3         11  
  3         115  
21 3     3   16 use Carp;
  3         5  
  3         233  
22 3     3   2705 use IO::File;
  3         35740  
  3         629  
23 3     3   31 use strict;
  3         6  
  3         105  
24 3     3   16 use warnings;
  3         16  
  3         82  
25              
26 3     3   3520 use bytes;
  3         34  
  3         17  
27              
28             #=====================================================================
29             # Package Global Variables:
30              
31             our $VERSION = '0.09';
32              
33             #=====================================================================
34             # Class AppleII::Disk:
35             #
36             # Member Variables:
37             # filename: The pathname of the disk image file
38             # writable: True if the image is opened in read/write mode
39             # file: The IO::File attached to the image file
40             # actlen: The size of the image file in bytes
41             # maxlen: The maximum allowable size of the image file in bytes
42             #---------------------------------------------------------------------
43             # Constructor:
44             #
45             # Input:
46             # filename:
47             # The pathname of the image file you want to open
48             # mode:
49             # A string indicating how the image should be opened
50             # May contain any of the following characters (case sensitive):
51             # r Allow reads (this is actually ignored; you can always read)
52             # w Allow writes
53             # d Disk image is in DOS 3.3 order (default)
54             # p Disk image is in ProDOS order
55              
56             sub new
57             {
58 4     4 1 438 my ($type, $filename, $mode) = @_;
59 4         11 my $self = {};
60 4         17 $self->{filename} = $filename;
61              
62 4         75 my $file = IO::File->new;
63              
64 4 50       216 $mode = 'r' unless $mode;
65 4         9 my $openMode = '<';
66 4 50       33 if ($mode =~ /w/) {
67 4         12 $self->{writable} = 1;
68 4         10 $openMode = '+<';
69 4 100       112 $openMode = '+>' if not -e $filename; # Create empty file
70             } # end if writable
71              
72 4 50       32 $file->open($filename, $openMode) or croak("Couldn't open `$filename': $!");
73 4         452 binmode $file; # binmode didn't become a method until IO::File 1.11
74              
75 4         14 $self->{file} = $file;
76 4         31 $self->{actlen} = ($file->stat)[7]; # Get real size of file
77 4         78 $self->{maxlen} = $self->{actlen};
78              
79 4 100       19 $type = 'AppleII::Disk::ProDOS' if $mode =~ /p/;
80 4 100       18 $type = 'AppleII::Disk::DOS33' if $mode =~ /d/;
81 4 50       29 $type = (($filename =~ /\.(?:hdv|po)$/i)
    100          
82             ? 'AppleII::Disk::ProDOS'
83             : 'AppleII::Disk::DOS33')
84             if ($type eq 'AppleII::Disk');
85 4         61 bless $self, $type;
86             } # end AppleII::Disk::new
87              
88             #---------------------------------------------------------------------
89             # Pad a block of data:
90             #
91             # This is a normal subroutine, NOT a method!
92             #
93             # Input:
94             # data: The block to be padded
95             # pad: The character to pad with (default "\0") or '' for no padding
96             # length: The length to pad to (default 0x200)
97             #
98             # Returns:
99             # The BLOCK padded to LENGTH with PAD
100             # Dies if the block is too long.
101             # If PAD is the null string, dies if BLOCK is not already LENGTH.
102              
103             sub pad_block
104             {
105 32     32 1 66 my ($data, $pad, $length) = @_;
106              
107 32 50       63 $pad = "\0" unless defined $pad;
108 32   100     108 $length = $length || 0x200;
109              
110 32 100 100     173 $data .= $pad x ($length - length($data))
111             if (length($pad) and length($data) < $length);
112              
113 32 100       71 unless (length($data) == $length) {
114 1         3 local $Carp::CarpLevel = $Carp::CarpLevel;
115 1 50       9 ++$Carp::CarpLevel if (caller)[0] =~ /^AppleII::Disk::/;
116 1         357 croak(sprintf("Data block is %d bytes",length($data)));
117             }
118              
119 31         86 $data;
120             } # end AppleII::Disk::pad_block
121              
122             #---------------------------------------------------------------------
123             # Get or set the disk size:
124             #
125             # Input:
126             # size: The number of blocks in the disk image
127             # If SIZE is omitted, the disk size is not changed
128             #
129             # Returns:
130             # The number of blocks in the disk image
131              
132             sub blocks
133             {
134 6     6 1 2238 my $self = shift;
135              
136 6 100       22 if (@_) {
137 3         16 $self->{maxlen} = $_[0] * 0x200;
138 3 50       13 carp "Disk image contains more than $_[0] blocks"
139             if $self->{maxlen} < $self->{actlen};
140             }
141              
142 6         43 int($self->{maxlen} / 0x200);
143             } # end AppleII::Disk::blocks
144              
145             #---------------------------------------------------------------------
146             # Extend the image file to its full size:
147              
148             sub fully_allocate
149             {
150 0     0 1 0 my $self = shift;
151              
152 0 0       0 if ($self->{maxlen} > $self->{actlen}) {
153 0 0       0 croak("Disk image is read/only") unless $self->{writable};
154              
155 0 0       0 $self->{file}->truncate($self->{maxlen}) or croak "Can't extend file: $!";
156              
157 0         0 $self->{actlen} = $self->{maxlen};
158             } # end if file is not already at maximum size
159              
160             } # end AppleII::Disk::fully_allocate
161              
162             #---------------------------------------------------------------------
163             # Read a ProDOS block:
164             #
165             # Input:
166             # block: The block number to read
167             #
168             # Returns:
169             # A 512 byte block
170             #
171             # Implemented in AppleII::Disk::ProDOS & AppleII::Disk::DOS33
172             #
173             # sub read_block
174              
175             #---------------------------------------------------------------------
176             # Read a series of ProDOS blocks:
177             #
178             # As a special case, block 0 cannot be read by this method. Instead,
179             # it returns a block full of 0 bytes. This is how sparse files are
180             # implemented. If you want to read the actual contents of block 0,
181             # you must call $disk->read_block(0) directly.
182             #
183             # Input:
184             # blocks: An array of block numbers to read
185             #
186             # Returns:
187             # The data from the disk (512 bytes times the number of blocks)
188              
189             sub read_blocks
190             {
191 10     10 1 27 my ($self, $blocks) = @_;
192 10         19 my $data = '';
193 10         30 foreach (@$blocks) {
194 1044 100       4876 if ($_) { $data .= $self->read_block($_) }
  24         117  
195 1020         1966 else { $data .= "\0" x 0x200 } # Sparse block
196             }
197 10         488 $data;
198             } # end AppleII::Disk::read_blocks
199              
200             #---------------------------------------------------------------------
201             # Read a DOS 3.3 sector:
202             #
203             # Input:
204             # track: The track number to read
205             # sector: The sector number to read
206             #
207             # Returns:
208             # A 256 byte sector
209             #
210             # Implemented in AppleII::Disk::ProDOS & AppleII::Disk::DOS33
211             #
212             # sub read_sector
213              
214             #---------------------------------------------------------------------
215             # Write a ProDOS block:
216             #
217             # Input:
218             # block: The block number to read
219             # data: The contents of the block
220             # pad: A character to pad the block with (optional)
221             # If PAD is omitted, an error is generated if data is not 512 bytes
222             #
223             # Implemented in AppleII::Disk::ProDOS & AppleII::Disk::DOS33
224             #
225             # sub write_block
226              
227             #---------------------------------------------------------------------
228             # Write a series of ProDOS blocks:
229             #
230             # As a special case, block 0 cannot be written by this method.
231             # Instead, that block is just skipped. This is how sparse files are
232             # implemented. If you want to write the contents of block 0, you must
233             # call $disk->write_block directly.
234             #
235             # Input:
236             # blocks: An array of the block numbers to write to
237             # data: The data to write (must be exactly the right size)
238             # pad: A character to pad the last block with (optional)
239              
240             sub write_blocks
241             {
242 5     5 1 42 my ($self, $blocks, $data, $pad) = @_;
243 5         10 my $index = 0;
244 5         13 foreach (@$blocks) {
245 271 100       9591 $self->write_block($_, substr($data, $index, 0x200), $pad) if $_;
246 271         318 $index += 0x200;
247             }
248             } # end AppleII::Disk::write_blocks
249              
250             #---------------------------------------------------------------------
251             # Write a DOS 3.3 sector:
252             #
253             # Input:
254             # track: The track number to read
255             # sector: The sector number to read
256             # data: The contents of the sector
257             # pad: The value to pad the sector with (optional)
258             # If PAD is omitted, an error is generated if data is not 256 bytes
259             #
260             # Implemented in AppleII::Disk::ProDOS & AppleII::Disk::DOS33
261             #
262             # sub write_sector
263              
264             #=====================================================================
265             package AppleII::Disk::ProDOS;
266             #
267             # Handle ProDOS-order disk images
268             #---------------------------------------------------------------------
269              
270 3     3   2950 use Carp;
  3         6  
  3         182  
271 3     3   14 use bytes;
  3         5  
  3         52  
272 3     3   2704 use integer;
  3         28  
  3         20  
273 3     3   78 use strict;
  3         5  
  3         83  
274 3     3   13 use warnings;
  3         5  
  3         1059  
275              
276             our @ISA = qw(AppleII::Disk);
277              
278             #---------------------------------------------------------------------
279             # Read a block from a ProDOS order disk:
280             #
281             # See AppleII::Disk::read_block
282              
283             sub read_block
284             {
285 56     56   2418 my $self = shift;
286              
287 56 100       223 return "\0" x 0x200
288             if $self->seek_block($_[0]) >= $self->{actlen}; # Past EOF
289 54         101 my $buffer = '';
290 54 50       867 read($self->{file},$buffer,0x200) or die;
291              
292 54         1106 $buffer;
293             } # end AppleII::Disk::ProDOS::read_block
294              
295             #---------------------------------------------------------------------
296             # FIXME AppleII::Disk::ProDOS::read_sector not implemented yet
297              
298             #---------------------------------------------------------------------
299             # Seek to the beginning of a block:
300             #
301             # Input:
302             # block: The block number to seek to
303             #
304             # Returns:
305             # The new position of the file pointer
306              
307             sub seek_block
308             {
309 76     76   117 my ($self, $block) = @_;
310              
311 76         114 my $pos = $block * 0x200;
312 76 100 66     660 croak("Invalid block number $block")
313             if $pos < 0 or $pos >= $self->{maxlen};
314              
315 75 50       329 $self->{file}->seek($pos,0) or die;
316              
317 75         11144 $pos;
318             } # end AppleII::Disk::ProDOS::seek_block
319              
320             #---------------------------------------------------------------------
321             # Write a block from a ProDOS order disk:
322             #
323             # See AppleII::Disk::write_block
324              
325             sub write_block
326             {
327 21     21   59 my ($self, $block, $data, $pad) = @_;
328 21 50       62 croak("Disk image is read/only") unless $self->{writable};
329              
330 21   100     82 $data = AppleII::Disk::pad_block($data, $pad || '');
331              
332 20         49 my $pos = $self->seek_block($block);
333 20 50       28 print {$self->{file}} $data or die;
  20         85  
334              
335 20 100       87 $self->{actlen} = $pos + 0x200 unless $self->{actlen} > $pos;
336             } # end AppleII::Disk::ProDOS::write_block
337              
338             #=====================================================================
339             package AppleII::Disk::DOS33;
340             #
341             # Handle DOS 3.3-order disk images
342             #---------------------------------------------------------------------
343              
344             #$debug = 1;
345              
346 3     3   17 use Carp;
  3         4  
  3         205  
347 3     3   15 use bytes;
  3         4  
  3         16  
348 3     3   75 use integer;
  3         6  
  3         9  
349 3     3   57 use strict;
  3         6  
  3         84  
350 3     3   19 use warnings;
  3         5  
  3         1989  
351              
352             our @ISA = qw(AppleII::Disk);
353              
354             #---------------------------------------------------------------------
355             # Convert ProDOS block number to track & sectors:
356              
357             { my @sector1 = ( 0, 13, 11, 9, 7, 5, 3, 1);
358             my @sector2 = (14, 12, 10, 8, 6, 4, 2, 15);
359              
360             sub block2sector
361             {
362 4     4   6 my $block = shift;
363 4         10 my $offset = $block % 8;
364              
365 4         16 ($block/8, $sector1[$offset], $sector2[$offset]); # INTEGER division
366             } # end block2sector
367             }
368              
369             #---------------------------------------------------------------------
370             # Read a block from a DOS 3.3 order disk:
371             #
372             # See AppleII::Disk::read_block
373              
374             sub read_block
375             {
376 3     3   383 my ($self, $block) = @_;
377 3         8 my ($track, $sector1, $sector2) = block2sector($block);
378              
379 3         12 $self->read_sector($track,$sector1) . $self->read_sector($track,$sector2);
380             } # end AppleII::Disk::DOS33::read_block
381              
382             #---------------------------------------------------------------------
383             # Read a DOS 3.3 sector:
384             #
385             # See AppleII::Disk::read_sector
386              
387             sub read_sector
388             {
389 7     7   915 my $self = shift;
390 7 100       22 return "\0" x 0x100
391             if $self->seek_sector(@_[0..1]) >= $self->{actlen}; # Past EOF
392 4         7 my $buffer = '';
393 4 50       101 read($self->{file},$buffer,0x100) or die;
394              
395 4         24 $buffer;
396             } # end AppleII::Disk::DOS33::read_sector
397              
398             #---------------------------------------------------------------------
399             # Seek to the beginning of a sector:
400             #
401             # Input:
402             # track: The track number to seek to
403             # sector: The sector number to seek to
404             #
405             # Returns:
406             # The new position of the file pointer
407              
408             sub seek_sector
409             {
410 13     13   21 my ($self, $track, $sector) = @_;
411              
412 13         24 my $pos = $track * 0x1000 + $sector * 0x100;
413 13 100 66     310 croak("Invalid position track $track sector $sector")
414             if $pos < 0 or $pos >= $self->{maxlen};
415              
416 12 50       84 $self->{file}->seek($pos,0) or die;
417 12         448 $pos;
418             } # end AppleII::Disk::DOS33::seek_sector
419              
420             #---------------------------------------------------------------------
421             # Write a sector to a DOS 3.3 order image:
422             #
423             # See AppleII::Disk::write_sector
424              
425             sub write_sector
426             {
427 6     6   1135 my ($self, $track, $sector, $data, $pad) = @_;
428 6 50       20 croak("Disk image is read/only") unless $self->{writable};
429              
430 6   100     28 $data = AppleII::Disk::pad_block($data, $pad || '', 0x100);
431              
432 6         16 my $pos = $self->seek_sector($track, $sector);
433 6 50       9 print {$self->{file}} $data or die;
  6         43  
434              
435 6 100       27 $self->{actlen} = $pos + 0x100 unless $self->{actlen} > $pos;
436             } # end AppleII::Disk::DOS33::write_sector
437              
438             #---------------------------------------------------------------------
439             # Write a block to a DOS33 order disk:
440             #
441             # See AppleII::Disk::write_block
442              
443             sub write_block
444             {
445 1     1   523 my ($self, $block, $data, $pad) = @_;
446 1 50       6 croak("Disk image is read/only") unless $self->{writable};
447 1         3 my ($track, $sector1, $sector2) = block2sector($block);
448              
449 1   50     8 $data = AppleII::Disk::pad_block($data, $pad || '');
450              
451 1         7 $self->write_sector($track, $sector1, substr($data,0,0x100));
452 1         6 $self->write_sector($track, $sector2, substr($data,0x100,0x100));
453             } # end AppleII::Disk::DOS33::write_block
454              
455             #=====================================================================
456             # Package Return Value:
457              
458             1;
459              
460             __END__