File Coverage

blib/lib/AppleII/ProDOS.pm
Criterion Covered Total %
statement 450 579 77.7
branch 84 166 50.6
condition 15 50 30.0
subroutine 68 83 81.9
pod 4 15 26.6
total 621 893 69.5


line stmt bran cond sub pod time code
1             #---------------------------------------------------------------------
2             package AppleII::ProDOS;
3             #
4             # Copyright 1996-2006 Christopher J. Madsen
5             #
6             # Author: Christopher J. Madsen
7             # Created: 26 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: Access files on Apple II ProDOS disk images
18             #---------------------------------------------------------------------
19              
20 2     2   60743 use 5.006;
  2         8  
  2         80  
21 2     2   1100 use AppleII::Disk 0.09;
  2         67  
  2         56  
22 2     2   12 use Carp;
  2         3  
  2         107  
23 2     2   1813 use POSIX 'mktime';
  2         17598  
  2         19  
24 2     2   4260 use bytes;
  2         4  
  2         18  
25 2     2   56 use strict;
  2         5  
  2         85  
26 2     2   11 use warnings;
  2         3  
  2         82  
27              
28 2     2   10 use Exporter 5.57 'import'; # exported import method
  2         65  
  2         4637  
29             our @ISA = qw(AppleII::ProDOS::Members);
30             our @EXPORT = qw();
31             our @EXPORT_OK = qw(
32             pack_date pack_name parse_date parse_name parse_type shell_wc
33             short_date unpack_date valid_date valid_name a2_croak
34             );
35              
36             my %vol_fields = (
37             bitmap => undef,
38             disk => undef,
39             diskSize => undef,
40             name => undef,
41             );
42              
43             # Methods to be passed along to the current directory:
44             my %dir_methods = (
45             catalog => undef,
46             get_file => undef,
47             new_dir => undef,
48             put_file => undef,
49             );
50              
51             #=====================================================================
52             # Package Global Variables:
53              
54             our $VERSION = '0.10';
55              
56             # Filetype list from About Apple II File Type Notes -- June 1992
57             my @filetypes = qw(
58             NON BAD PCD PTX TXT PDA BIN FNT FOT BA3 DA3 WPF SOS $0D $0E DIR
59             RPD RPI AFD AFM AFR SCL PFS $17 $18 ADB AWP ASP $1C $1D $1E $1F
60             TDM $21 $22 $23 $24 $25 $26 $27 $28 $29 8SC 8OB 8IC 8LD P8C $2F
61             $30 $31 $32 $33 $34 $35 $36 $37 $38 $39 $3A $3B $3C $3D $3E $3F
62             DIC $41 FTD $43 $44 $45 $46 $47 $48 $49 $4A $4B $4C $4D $4E $4F
63             GWP GSS GDB DRW GDP HMD EDU STN HLP COM CFG ANM MUM ENT DVU FIN
64             $60 $61 $62 $63 $64 $65 $66 $67 $68 $69 $6A BIO $6C TDR PRE HDV
65             $70 $71 $72 $73 $74 $75 $76 $77 $78 $79 $7A $7B $7C $7D $7E $7F
66             $80 $81 $82 $83 $84 $85 $86 $87 $88 $89 $8A $8B $8C $8D $8E $8F
67             $90 $91 $92 $93 $94 $95 $96 $97 $98 $99 $9A $9B $9C $9D $9E $9F
68             WP $A1 $A2 $A3 $A4 $A5 $A6 $A7 $A8 $A9 $AA GSB TDF BDF $AE $AF
69             SRC OBJ LIB S16 RTL EXE PIF TIF NDA CDA TOL DVR LDF FST $BE DOC
70             PNT PIC ANI PAL $C4 OOG SCR CDV FON FND ICN $CB $CC $CD $CE $CF
71             $D0 $D1 $D2 $D3 $D4 MUS INS MDI SND $D9 $DA DBM $DC $DD $DE $DF
72             LBR $E1 ATK $E3 $E4 $E5 $E6 $E7 $E8 $E9 $EA $EB $EC $ED R16 PAS
73             CMD $F1 $F2 $F3 $F4 $F5 $F6 $F7 $F8 OS INT IVR BAS VAR REL SYS
74             ); # end filetypes
75              
76             #=====================================================================
77             # package AppleII::ProDOS:
78             #
79             # Member Variables:
80             # bitmap:
81             # An AppleII::ProDOS::Bitmap containing the volume bitmap
82             # directories:
83             # Array of AppleII::ProDOS::Directory starting with the volume dir
84             # disk:
85             # The AppleII::Disk we are accessing
86             # diskSize:
87             # The number of blocks on the disk
88             # name:
89             # The volume name of the disk
90             #---------------------------------------------------------------------
91             # Constructor for creating a new disk:
92             #
93             # Input:
94             # name:
95             # The volume name for the new disk
96             # diskSize:
97             # The size of the disk in blocks
98             # filename:
99             # The pathname of the image file you want to open
100             # mode: (optional)
101             # A string indicating how the image should be opened
102             # See AppleII::Disk::new for details.
103             # 'rw' is always appended to the mode
104              
105             sub new
106             {
107 0     0 1 0 my ($type, $name, $diskSize, $filename, $mode) = @_;
108              
109 0 0       0 a2_croak("Invalid name `$name'") unless valid_name($name);
110 0         0 $name = uc $name;
111              
112 0   0     0 my $disk = AppleII::Disk->new($filename, ($mode || '') . 'rw');
113 0         0 $disk->blocks($diskSize);
114              
115 0         0 my $bitmap = AppleII::ProDOS::Bitmap->new($disk,6,$diskSize);
116              
117 0         0 my $self = {
118             bitmap => $bitmap,
119             directories => [ AppleII::ProDOS::Directory->new(
120             $name, $disk, [2 .. 5], $bitmap
121             ) ],
122             disk => $disk,
123             name => $name,
124             _dir_methods => \%dir_methods,
125             _permitted => \%vol_fields,
126             };
127              
128 0         0 $bitmap->write_disk;
129 0         0 $self->{directories}[0]->write_disk;
130              
131 0         0 bless $self, $type;
132             } # end AppleII::ProDOS::new
133              
134             #---------------------------------------------------------------------
135             # Constructor for opening an existing disk:
136             #
137             # There are two forms:
138             # open(disk); or
139             # open(filename, mode);
140             #
141             # Input:
142             # disk:
143             # The AppleII::Disk to use
144             # filename:
145             # The pathname of the image file you want to open
146             # mode:
147             # A string indicating how the image should be opened
148             # May contain any of the following characters (case sensitive):
149             # r Allow reads (this is actually ignored; you can always read)
150             # w Allow writes
151              
152             sub open
153             {
154 1     1 1 19847 my ($type, $disk, $mode) = @_;
155 1         11 my $self = {
156             _dir_methods => \%dir_methods,
157             _permitted => \%vol_fields,
158             };
159 1 50       23 $disk = AppleII::Disk->new($disk, $mode) unless ref $disk;
160 1         4 $self->{disk} = $disk;
161              
162 1         5 my $volDir = $disk->read_block(2);
163              
164 1         3 my $storageType;
165 1         7 ($storageType, $self->{name}) = parse_name(substr($volDir,0x04,16));
166 1 50       6 croak('This is not a ProDOS disk') unless $storageType == 0xF;
167              
168 1         5 my ($startBlock, $diskSize) = unpack('x39v2',$volDir);
169 1         10 $disk->blocks($diskSize);
170              
171 1         9 $self->{bitmap} =
172             AppleII::ProDOS::Bitmap->open($disk,$startBlock,$diskSize);
173              
174 1         9 $self->{directories} = [
175             AppleII::ProDOS::Directory->open($disk, 2, $self->{bitmap})
176             ];
177 1         3 $self->{diskSize} = $diskSize;
178              
179 1         6 bless $self, $type;
180             } # end AppleII::ProDOS::open
181              
182             #---------------------------------------------------------------------
183             # Return the current directory:
184             #
185             # Returns:
186             # The current AppleII::ProDOS::Directory
187              
188             sub dir {
189 0     0 1 0 shift->{directories}[-1];
190             } # end AppleII::ProDOS::dir
191              
192             #---------------------------------------------------------------------
193             # Return or change the current path:
194             #
195             # Input:
196             # newpath: The path to change to
197             #
198             # Returns:
199             # The current path (begins and ends with '/')
200              
201             sub path
202             {
203 3     3 1 10 my ($self, $newpath) = @_;
204              
205 3 50       13 if ($newpath) {
206             # Change directory:
207 3         6 my @directories = @{$self->{directories}};
  3         12  
208 3 100       48 $#directories = 0 if $newpath =~ s!^/\Q$self->{name}\E/?!!i;
209             pop @directories
210 3   33     16 while $#directories and $newpath =~ s'^\.\.(?:/|$)''; #'
211 3         5 my $dir;
212 3         11 foreach $dir (split(/\//, $newpath)) {
213 2         4 eval { push @directories, $directories[-1]->open_dir($dir) };
  2         10  
214 2 50       10 a2_croak("No such directory `$_[1]'")
215             if $@ =~ /^LibA2: No such directory/;
216 2 50       9 die $@ if $@;
217             }
218 3         12 $self->{directories} = \@directories;
219             } # end if changing path
220              
221 3         11 '/'.join('/',map { $_->{name} } @{$self->{directories}}).'/';
  5         36  
  3         8  
222             } # end AppleII::ProDOS::path
223              
224             #---------------------------------------------------------------------
225             # Pass method calls along to the current directory:
226              
227             sub AUTOLOAD
228             {
229 19     19   10029 my $self = $_[0];
230 19         36 my $name = our $AUTOLOAD;
231 19         198 $name =~ s/.*://; # strip fully-qualified portion
232 19 100 66     165 unless (ref($self) and exists $self->{'_dir_methods'}{$name}) {
233             # Try to access a field by that name:
234 3         6 $AppleII::ProDOS::Members::AUTOLOAD = $AUTOLOAD;
235 3         28 goto &AppleII::ProDOS::Members::AUTOLOAD;
236             }
237              
238 16         45 shift @_; # Remove self
239 16         105 $self->{directories}[-1]->$name(@_);
240             } # end AppleII::ProDOS::AUTOLOAD
241              
242             #---------------------------------------------------------------------
243             # Like croak, but get out of all AppleII::ProDOS classes:
244              
245             sub a2_croak
246             {
247 0     0 0 0 local $Carp::CarpLevel = $Carp::CarpLevel;
248 0         0 while ((caller $Carp::CarpLevel)[0] =~ /^AppleII::ProDOS/) {
249 0         0 ++$Carp::CarpLevel;
250             }
251 0         0 croak("LibA2: " . $_[0]);
252             } # end AppleII::ProDOS::a2_croak
253              
254             #---------------------------------------------------------------------
255             # Convert a time to ProDOS format:
256             #
257             # This is NOT a method; it's just a regular subroutine.
258             #
259             # Input:
260             # time: The time to convert
261             #
262             # Returns:
263             # Packed string
264              
265             sub pack_date
266             {
267 4 50   4 0 532 if (@_ == 1) { # Unix timestamp
    100          
    50          
268 0         0 @_ = (localtime($_[0]))[5,4,3,2,1];
269 0         0 ++$_[1];
270             } elsif (@_ == 3) { # Year, Month, Day
271 1         3 push @_, 0, 0; # Hour, Minute
272             } elsif (@_ < 5) {
273 0         0 croak "Usage: pack_date(TIMESTAMP | Y,M,D | Y,M,D,H,M)";
274             }
275              
276 4         63 pack('vC2', (($_[0]%100)<<9) + ($_[1]<<5) + $_[2], @_[4,3]);
277             } # end AppleII::ProDOS::pack_date
278              
279             #---------------------------------------------------------------------
280             # Convert a filename to ProDOS format (length nibble):
281             #
282             # This is NOT a method; it's just a regular subroutine.
283             #
284             # Input:
285             # type: The high nibble of the type/length byte
286             # name: The name
287             #
288             # Returns:
289             # Packed string
290              
291             sub pack_name
292             {
293 9     9 0 42 pack('Ca15',($_[0] << 4) + length($_[1]), uc $_[1]);
294             } # end AppleII::ProDOS::pack_name
295              
296             #---------------------------------------------------------------------
297             # Extract a date & time:
298             #
299             # This is NOT a method; it's just a regular subroutine.
300             #
301             # Input:
302             # dateField: The date/time field
303             #
304             # Returns:
305             # Standard time for use with gmtime (not localtime)
306             # undef if no date
307              
308             sub parse_date
309             {
310 0     0 0 0 my ($date, $minute, $hour) = unpack('vC2', $_[0]);
311 0 0       0 return undef unless $date;
312 0         0 my ($year, $month, $day) = ($date>>9, (($date>>5) & 0x0F), $date & 0x1F);
313 0         0 mktime(0, $minute, $hour, $day, $month-1, $year);
314             } # end AppleII::ProDOS::parse_date
315              
316             #---------------------------------------------------------------------
317             # Extract a filename:
318             #
319             # This is NOT a method; it's just a regular subroutine.
320             #
321             # Input:
322             # nameField: The type/length byte followed by the name
323             #
324             # Returns:
325             # (type, name)
326              
327             sub parse_name
328             {
329 94     94 0 122 my $typeLen = ord $_[0];
330 94         244 ($typeLen >> 4, substr($_[0],1,$typeLen & 0x0F));
331             } # end AppleII::ProDOS::parse_name
332              
333             #---------------------------------------------------------------------
334             # Convert a filetype to its abbreviation:
335             #
336             # This is NOT a method; it's just a regular subroutine.
337             #
338             # Input:
339             # type: The filetype to convert (0-255)
340             #
341             # Returns:
342             # The abbreviation for the filetype
343              
344             sub parse_type
345             {
346 12     12 0 71 $filetypes[$_[0]];
347             } # end AppleII::ProDOS::parse_type
348              
349             #---------------------------------------------------------------------
350             # Convert shell-type wildcards to Perl regexps:
351             #
352             # This is NOT a method; it's just a regular subroutine.
353             #
354             # Input:
355             # The filename with optional wildcards
356             #
357             # Returns:
358             # A Perl regexp
359              
360             sub shell_wc
361             {
362 0         0 '^' .
363             join('',
364 0 0   0 0 0 map { if (/\?/) {'.'} elsif (/\*/) {'.*'} else {quotemeta $_}}
  0 0       0  
  0         0  
  0         0  
365             split(//,$_[0]));
366             } # end AppleII::ProDOS::shell_wc
367              
368             #---------------------------------------------------------------------
369             # Convert a date & time to a short string:
370             #
371             # This is NOT a method; it's just a regular subroutine.
372             #
373             # Input:
374             # dateField: The date/time field
375             #
376             # Returns:
377             # "dd-Mmm-yy hh:mm" or " "
378              
379             my @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
380              
381             sub short_date
382             {
383 24     24 0 77 my ($date, $minute, $hour) = unpack('vC2', $_[0]);
384 24 100       61 return " " unless $date;
385 22         50 my ($year, $month, $day) = ($date>>9, (($date>>5) & 0x0F), $date & 0x1F);
386 22         185 sprintf('%2d-%s-%02d %2d:%02d',$day,$months[$month-1],$year,$hour,$minute);
387             } # end AppleII::ProDOS::short_date
388              
389             #---------------------------------------------------------------------
390             # Convert a date & time to Date::Calc format:
391             #
392             # This is NOT a method; it's just a regular subroutine.
393             #
394             # Input:
395             # dateField: The date/time field
396             #
397             # Returns:
398             # (YEAR, MONTH, DAY, HOUR, MINUTE)
399             # The empty list if the date is null
400              
401             sub unpack_date
402             {
403 2     2 0 1371 my ($date, $minute, $hour) = unpack('vC2', $_[0]);
404 2 50       8 return unless $date;
405              
406 2         5 my $year = $date >> 9;
407              
408 2 50       23 return ((($year < 77) ? $year + 2000 : $year + 1900),
409             (($date>>5) & 0x0F), $date & 0x1F, $hour, $minute);
410             } # end AppleII::ProDOS::unpack_date
411              
412             #---------------------------------------------------------------------
413             # Determine if a date is valid:
414             #
415             # May be called as a method or a normal subroutine.
416             #
417             # This is not a very strenuous check; it doesn't know that not all
418             # months have 31 days. [FIXME]
419             #
420             # Input:
421             # The date to check in ProDOS format (4 byte packed string)
422             #
423             # Returns:
424             # 0 if the date is invalid
425             # 1 if the date is zero (no date)
426             # 2 if the date is valid
427              
428             sub valid_date
429             {
430 2 50   2 0 6 return 1 if $_[-1] eq "\0\0\0\0"; # No date
431 2         8 my ($date, $minute, $hour) = unpack('vC2', $_[-1]);
432 2         6 my ($year, $month, $day) = ($date>>9, (($date>>5) & 0x0F), $date & 0x1F);
433 2 50 33     50 return 0 if $minute > 59 or $hour > 23 or $year > 99
      33        
      33        
      33        
      33        
      33        
434             or $month > 12 or $month < 1 or $day > 31 or $day < 1;
435 2         4 2; # Valid date
436             } # end AppleII::ProDOS::valid_date
437              
438             #---------------------------------------------------------------------
439             # Determine if a filename is valid:
440             #
441             # May be called as a method or a normal subroutine.
442             #
443             # Input:
444             # The file to check
445             #
446             # Returns:
447             # True if the filename is valid
448              
449             sub valid_name
450             {
451 2     2 0 21 $_[-1] =~ /\A[a-z][a-z0-9.]{0,14}\Z(?!\n)/i;
452             } # end AppleII::ProDOS::valid_name
453              
454             #=====================================================================
455             package AppleII::ProDOS::Bitmap;
456             #
457             # Member Variables:
458             # bitmap: The volume bitmap itself
459             # blocks: An array of the block numbers where the bitmap is stored
460             # disk: An AppleII::Disk
461             # diskSize: The number of blocks on the disk
462             # free: The number of free blocks
463             #---------------------------------------------------------------------
464              
465 2     2   16 use Carp;
  2         10  
  2         147  
466 2     2   14 use bytes;
  2         4  
  2         11  
467 2     2   59 use strict;
  2         11  
  2         68  
468 2     2   10 use warnings;
  2         10  
  2         2047  
469              
470             our @ISA = 'AppleII::ProDOS::Members';
471              
472             # Map ProDOS bit order to Perl's vec():
473             my @adjust = (7, 5, 3, 1, -1, -3, -5, -7);
474              
475             my %bit_fields = (
476             diskSize => undef,
477             free => undef,
478             );
479              
480             #---------------------------------------------------------------------
481             # Constructor for creating a new bitmap:
482             #
483             # All blocks are marked free, except for blocks 0 thru the end of the
484             # bitmap, which are marked used.
485             #
486             # Input:
487             # disk: The AppleII::Disk to use
488             # startBlock: The block number where the volume bitmap begins
489             # diskSize: The size of the disk in blocks
490              
491             sub new
492             {
493 0     0   0 my ($type, $disk, $startBlock, $diskSize) = @_;
494 0         0 my $self = {
495             bitmap => ("\xFF" x int($diskSize / 8)),
496             disk => $disk,
497             diskSize => $diskSize,
498             free => $diskSize,
499             _permitted => \%bit_fields,
500             };
501 0         0 bless $self, $type;
502 0         0 $self->mark([ $diskSize-8 .. $diskSize-1], 1); # Mark odd blocks at end
503              
504 0         0 my @blocks;
505 0         0 do {
506 0         0 push @blocks, $startBlock++;
507             } while ($diskSize -= 0x1000) > 0;
508              
509 0         0 $self->mark([ 0 .. $blocks[-1] ], 0); # Mark initial blocks as used
510              
511 0         0 $self->{bitmap} =
512             AppleII::Disk::pad_block($self->{bitmap},"\0",($#blocks+1) * 0x200);
513 0         0 $self->{blocks} = \@blocks;
514 0         0 $self->{free} = unpack('%32b*', $self->{bitmap});
515              
516 0         0 $self;
517             } # end AppleII::ProDOS::Bitmap::new
518              
519             #---------------------------------------------------------------------
520             # Constructor for reading an existing bitmap:
521             #
522             # Input:
523             # disk: The AppleII::Disk to use
524             # startBlock: The block number where the volume bitmap begins
525             # diskSize: The size of the disk in blocks
526             # STARTBLOCK & BLOCKS are optional. If they are omitted, we get
527             # the information from the volume directory.
528              
529             sub open
530             {
531 1     1   4 my ($type, $disk, $startBlock, $diskSize) = @_;
532 1         2 my $self = {};
533 1         4 $self->{disk} = $disk;
534 1         3 $self->{'_permitted'} = \%bit_fields;
535 1 50 33     9 unless ($startBlock and $diskSize) {
536 0         0 my $volDir = $disk->read_block(2);
537 0         0 ($startBlock, $diskSize) = unpack('v2',substr($volDir,0x27,4));
538             }
539 1         3 $self->{diskSize} = $diskSize;
540 1         2 do {
541 1         2 push @{$self->{blocks}}, $startBlock++;
  1         9  
542             } while ($diskSize -= 0x1000) > 0;
543              
544 1         4 bless $self, $type;
545 1         52 $self->read_disk;
546 1         3 $self;
547             } # end AppleII::ProDOS::Bitmap::open
548              
549             #---------------------------------------------------------------------
550             # Get some free blocks:
551             #
552             # Input:
553             # count: The number of blocks requested
554             #
555             # Returns:
556             # A list of block numbers (which have been marked as used)
557             # The empty list if there aren't enough free blocks
558              
559             sub get_blocks
560             {
561 2     2   5 my ($self, $count) = @_;
562 2 50       9 return () if $count > $self->{free};
563 2         3 my @blocks;
564 2         7 my $bitmap = $self->{bitmap};
565             BLOCK:
566 2         15 while ($bitmap =~ m/([^\0])/g) {
567 4         23 my ($offset, $byte) = (8*pos($bitmap)-9, unpack('B8',$1));
568 4         17 while ($byte =~ m/1/g) {
569 13         17 push @blocks, $offset + pos($byte);
570 13 100       46 last BLOCK unless --$count;
571             }
572             } # end while BLOCK
573 2 50       5 return () if $count; # We couldn't find enough
574 2         8 $self->mark(\@blocks,0); # Mark blocks as in use
575 2         79 @blocks;
576             } # end AppleII::ProDOS::Bitmap::get_blocks
577              
578             #---------------------------------------------------------------------
579             # See if a block is free:
580             #
581             # This method is not currently used and may be removed.
582             #
583             # Input:
584             # block: The block number to check
585             #
586             # Returns:
587             # True if the block is free
588              
589             sub is_free
590             {
591 0     0   0 my ($self, $block) = @_;
592 0 0 0     0 croak("No block $block") if $block < 0 or $block >= $self->{diskSize};
593 0         0 vec($self->{bitmap}, $block + $adjust[$block % 8],1);
594             } # end AppleII::ProDOS::Bitmap::is_free
595              
596             #---------------------------------------------------------------------
597             # Mark blocks as free or used:
598             #
599             # Input:
600             # blocks: A block number or list of block numbers to mark
601             # mark: 1 for Free, 0 for Used
602              
603             sub mark
604             {
605 2     2   6 my ($self, $blocks, $mark) = @_;
606 2         5 my $diskSize = $self->{diskSize};
607 2 50       7 $blocks = [ $blocks ] unless ref $blocks;
608              
609 2         3 my $block;
610 2         6 foreach $block (@$blocks) {
611 13 50 33     52 croak("No block $block") if $block < 0 or $block >= $diskSize;
612 13         41 vec($self->{bitmap}, $block + $adjust[$block % 8],1) = $mark;
613             }
614 2 50       12 $self->{free} += ($mark ? 1 : -1) * ($#$blocks + 1);
615             } # end AppleII::ProDOS::Bitmap::mark
616              
617             #---------------------------------------------------------------------
618             # Read bitmap from disk:
619              
620             sub read_disk
621             {
622 1     1   3 my $self = shift;
623 1         16 $self->{bitmap} = $self->{disk}->read_blocks($self->{blocks});
624 1         9 $self->{free} = unpack('%32b*', $self->{bitmap});
625             } # end AppleII::ProDOS::Bitmap::read_disk
626              
627             #---------------------------------------------------------------------
628             # Return the block number where the bitmap begins:
629              
630             sub start_block
631             {
632 0     0   0 shift->{blocks}[0];
633             } # end AppleII::ProDOS::Bitmap::start_block
634              
635             #---------------------------------------------------------------------
636             # Write bitmap to disk:
637              
638             sub write_disk
639             {
640 2     2   4 my $self = shift;
641 2         11 $self->{disk}->write_blocks($self->{blocks}, $self->{bitmap});
642             } # end AppleII::ProDOS::Bitmap::write_disk
643              
644             #=====================================================================
645             package AppleII::ProDOS::Directory;
646             #
647             # Member Variables:
648             # access:
649             # The access attributes for this directory
650             # bitmap:
651             # The AppleII::ProDOS::Bitmap for the disk
652             # blocks:
653             # The list of blocks used by this directory
654             # disk:
655             # An AppleII::Disk
656             # entries:
657             # The list of directory entries
658             # name:
659             # The directory name
660             # created:
661             # The date/time the directory was created
662             # reserved:
663             # The contents of the reserved section (8 byte string)
664             # type:
665             # 0xF for a volume directory, 0xE for a subdirectory
666             # version:
667             # The contents of the VERSION & MIN_VERSION (2 byte string)
668             #
669             # For subdirectories:
670             # parent: The block number in the parent directory where our entry is
671             # parentNum: Our entry number within that block of the parent directory
672             # fixParent: True means our parent entry needs to be updated
673             #
674             # We also use the os_openDirs field of the disk to keep track of open
675             # directories. It contains a hash of Directory objects indexed by key
676             # block. The constructors automatically add the new objects to the
677             # hash, and the destructor removes them.
678             #---------------------------------------------------------------------
679              
680             AppleII::ProDOS->import(qw(a2_croak pack_date pack_name parse_name
681             short_date valid_date valid_name));
682 2     2   57 use Carp;
  2         3  
  2         119  
683 2     2   8 use bytes;
  2         3  
  2         13  
684 2     2   46 use strict;
  2         3  
  2         62  
685 2     2   8 use warnings;
  2         4  
  2         5351  
686              
687             our @ISA = 'AppleII::ProDOS::Members';
688              
689             my %dir_fields = (
690             access => 0xFF,
691             created => \&valid_date,
692             name => \&valid_name,
693             type => undef,
694             version => undef,
695             );
696              
697             #---------------------------------------------------------------------
698             # Constructor for creating a new directory:
699             #
700             # You must supply parent & parentNum when creating a subdirectory.
701             #
702             # Input:
703             # name: The name of the new directory
704             # disk: An AppleII::Disk
705             # blocks: A block number or array of block numbers for the directory
706             # bitmap: The AppleII::ProDOS::Bitmap for the disk
707             # parent: The block number in the parent directory where our entry is
708             # parentNum: Our entry number within that block of the parent directory
709              
710             sub new
711             {
712 0     0   0 my ($type, $name, $disk, $blocks, $bitmap, $parent, $parentNum) = @_;
713              
714 0 0       0 a2_croak("Invalid name `$name'") unless valid_name($name);
715              
716 0         0 my $self = {
717             access => 0xE3,
718             bitmap => $bitmap,
719             blocks => $blocks,
720             disk => $disk,
721             entries => [],
722             name => uc $name,
723             version => "\0\0",
724             created => pack_date(time),
725             _permitted => \%dir_fields,
726             };
727              
728 0 0       0 if ($parent) {
729 0         0 $self->{type} = 0xE; # Subdirectory
730 0         0 $self->{parent} = $parent;
731 0         0 $self->{parentNum} = $parentNum;
732 0         0 $self->{reserved} = "\x75\x23\x00\xC3\x27\x0D\x00\x00";
733             } else {
734 0         0 $self->{type} = 0xF; # Volume directory
735 0         0 $self->{reserved} = "\0" x 8; # 8 bytes reserved
736             } # end else volume directory
737              
738 0         0 bless $self, $type;
739 0         0 $disk->{os_openDirs}{$blocks->[0]} = $self;
740 0         0 $self;
741             } # end AppleII::ProDOS::Directory::new
742              
743             #---------------------------------------------------------------------
744             # Constructor for reading an existing directory:
745             #
746             # Input:
747             # disk: An AppleII::Disk
748             # block: The block number where the directory begins
749             # bitmap: The AppleII::ProDOS::Bitmap for the disk
750              
751             sub open
752             {
753 3     3   16 my ($type, $disk, $block, $bitmap) = @_;
754 3         19 my $self = {
755             bitmap => $bitmap,
756             disk => $disk,
757             _permitted => \%dir_fields,
758             };
759              
760 3         13 bless $self, $type;
761 3         10 $disk->{os_openDirs}{$block} = $self;
762 3         14 $self->read_disk($block);
763 3         11 $self;
764             } # end AppleII::ProDOS::Directory::open
765              
766             #---------------------------------------------------------------------
767             # Destructor:
768             #
769             # Removes the directory from the hash of open directories.
770              
771             sub DESTROY
772             {
773 1     1   2 my $self = shift;
774 1         33 delete $self->{disk}{os_openDirs}{$self->{blocks}[0]};
775             } # end AppleII::ProDOS::Directory::DESTROY
776              
777             #---------------------------------------------------------------------
778             # Add entry:
779             #
780             # Dies if the entry can't be added.
781             #
782             # Input:
783             # entry: An AppleII::ProDOS::DirEntry
784              
785             sub add_entry
786             {
787 2     2   4 my ($self,$entry) = @_;
788              
789 2 50       22 a2_croak($entry->name . ' already exists')
790             if $self->find_entry($entry->name);
791              
792 2         6 my $entries = $self->{entries};
793              
794 2         3 my $i;
795 2         7 for ($i=0; $i <= $#$entries; ++$i) {
796 5 50       22 last if $entries->[$i]{num} > $i+1;
797             }
798              
799 2 50       4 if ($i+1 >= 0xD * scalar @{$self->{blocks}}) {
  2         8  
800 0 0       0 a2_croak('Volume full') unless $self->{type} == 0xE; # Subdirectory
801 0         0 my @blocks = $self->{bitmap}->get_blocks(1);
802 0 0       0 a2_croak('Volume full') unless @blocks;
803 0         0 push @{$self->{blocks}}, @blocks;
  0         0  
804 0         0 $self->{fixParent} = 1;
805             } # end if directory full
806              
807 2         9 $entry->{num} = $i+1;
808 2         6 splice @$entries, $i, 0, $entry;
809             } # end AppleII::ProDOS::Directory::add_entry
810              
811             #---------------------------------------------------------------------
812             # Return the directory listing and free space information:
813             #
814             # Returns:
815             # A string containing the catalog in ProDOS format
816              
817             sub catalog
818             {
819 4     4   9 my $self = shift;
820 4         7 my $result =
821             sprintf("%-15s%s %s %-14s %-14s %8s %s\n",
822             qw(Name Type Blocks Modified Created Size Subtype));
823 4         8 my $entry;
824 4         8 foreach $entry (@{$self->{entries}}) {
  4         17  
825 12         70 $result .= sprintf("%-15s %-3s %5d %s %s %8d \$%04X\n",
826             $entry->name, $entry->short_type, $entry->blksUsed,
827             short_date($entry->modified),
828             short_date($entry->created),
829             $entry->size, $entry->auxtype);
830             } # end foreach entry
831              
832 4         12 my $bitmap = $self->{bitmap};
833 4         23 my ($free, $total, $used) = ($bitmap->free, $bitmap->diskSize);
834 4         9 $used = $total - $free;
835              
836 4         63 $result .
837             "Blocks free: $free Blocks used: $used Total blocks: $total\n";
838             } # end AppleII::ProDOS::Directory::catalog
839              
840             #---------------------------------------------------------------------
841             # Return the list of entries:
842             #
843             # Returns:
844             # A list of AppleII::ProDOS::DirEntry objects
845              
846             sub entries
847             {
848 0     0   0 @{shift->{entries}};
  0         0  
849             } # end AppleII::ProDOS::Directory::entries
850              
851             #---------------------------------------------------------------------
852             # Find an entry:
853             #
854             # Input:
855             # filename: The filename to match
856             #
857             # Returns:
858             # The entry representing that filename
859              
860             sub find_entry
861             {
862 14     14   27 my ($self, $filename) = @_;
863 14         29 $filename = uc $filename;
864 14         22 (grep {uc($_->name) eq $filename} @{$self->{'entries'}})[0];
  42         179  
  14         40  
865             } # end AppleII::ProDOS::Directory::find_entry
866              
867             #---------------------------------------------------------------------
868             # Read a file:
869             #
870             # Input:
871             # file:
872             # The name of the file to read, OR
873             # an AppleII::ProDOS::DirEntry object representing a file
874             #
875             # Returns:
876             # A new AppleII::ProDOS::File object for the file
877              
878             sub get_file
879             {
880 10     10   21 my ($self, $filename) = @_;
881              
882 10 50 33     52 my $entry = (ref($filename)
883             ? $filename
884             : ($self->find_entry($filename)
885             or a2_croak("No such file `$filename'")));
886              
887 10         56 AppleII::ProDOS::File->open($self->{disk}, $entry);
888             } # end AppleII::ProDOS::Directory::get_file
889              
890             #---------------------------------------------------------------------
891             # List files matching a regexp:
892             #
893             # Input:
894             # pattern:
895             # The Perl regexp to match
896             # (AppleII::ProDOS::shell_wc converts shell-type wildcards to regexps)
897             # filter: (optional)
898             # A subroutine to run against the entries
899             # It must return a true value for the file to be accepted.
900             # There are three special values:
901             # undef Match anything
902             # 'DIR' Match only directories
903             # '!DIR' Match anything but directories
904             #
905             # Returns:
906             # A list of filenames matching the pattern
907              
908             sub list_matches
909             {
910 0     0   0 my ($self, $pattern, $filter) = @_;
911 0 0       0 $filter = \&is_dir if $filter eq 'DIR';
912 0 0       0 $filter = \&isnt_dir if $filter eq '!DIR';
913 0 0       0 $filter = \&true unless $filter;
914 0 0 0     0 map { ($_->name =~ /$pattern/i and &$filter($_))
  0         0  
915             ? $_->name
916             : () }
917 0         0 @{$self->{'entries'}};
918             } # end AppleII::ProDOS::Directory::list_matches
919              
920 0     0   0 sub is_dir { $_[0]->type == 0x0F } # True if entry is directory
921 0     0   0 sub isnt_dir { $_[0]->type != 0x0F } # True if entry is not directory
922 0     0   0 sub true { 1 } # Accept anything
923              
924             #---------------------------------------------------------------------
925             # Create a subdirectory:
926             #
927             # Input:
928             # dir: The name of the subdirectory to create
929             # size: The number of entries the directory should hold
930             # The default is to create a 1 block directory
931             #
932             # Returns:
933             # The DirEntry object for the new directory
934              
935             sub new_dir
936             {
937 0     0   0 my ($self, $dir, $size) = @_;
938              
939 0 0       0 a2_croak("Invalid name `$dir'") unless valid_name($dir);
940 0         0 $dir = uc $dir;
941              
942 0 0       0 $size = 1 unless $size;
943 0         0 $size = int(($size + 0xD) / 0xD); # Compute # of blocks (+ dir header)
944              
945 0 0       0 my @blocks = $self->{bitmap}->get_blocks($size)
946             or a2_croak("Not enough free space");
947              
948 0         0 my $entry = AppleII::ProDOS::DirEntry->new;
949              
950 0         0 eval {
951 0         0 $entry->storage(0xD); # Directory
952 0         0 $entry->name($dir);
953 0         0 $entry->type(0x0F); # Directory
954 0         0 $entry->block($blocks[0]);
955 0         0 $entry->blksUsed($#blocks + 1);
956 0         0 $entry->size(0x200 * ($#blocks + 1));
957              
958 0         0 $self->add_entry($entry);
959 0         0 my $subdir = AppleII::ProDOS::Directory->new(
960             $dir, $self->{disk}, \@blocks, $self->{bitmap},
961             $self->{blocks}[int($entry->num / 0xD)], int($entry->num % 0xD)+1
962             );
963              
964 0         0 $subdir->write_disk;
965 0         0 $self->write_disk;
966 0         0 $self->{bitmap}->write_disk;
967             }; # end eval
968 0 0       0 if ($@) {
969 0         0 my $error = $@; # Clean up after error
970 0         0 $self->read_disk;
971 0         0 $self->{bitmap}->read_disk;
972 0         0 die $error;
973             } # end if error while creating directory
974              
975 0         0 $entry;
976             } # end AppleII::ProDOS::Directory::new_dir
977              
978             #---------------------------------------------------------------------
979             # Open a subdirectory:
980             #
981             # Input:
982             # dir: The name of the subdirectory to open, OR
983             # an AppleII::ProDOS::DirEntry object representing the directory
984             #
985             # Returns:
986             # A new AppleII::ProDOS::Directory object for the subdirectory
987              
988             sub open_dir
989             {
990 2     2   4 my ($self, $dir) = @_;
991              
992 2 50 33     15 my $entry = (ref($dir)
993             ? $dir
994             : ($self->find_entry($dir)
995             or a2_croak("No such directory `$dir'")));
996              
997 2 50       18 a2_croak('`' . $entry->name . "' is not a directory")
998             unless $entry->type == 0x0F;
999              
1000 2         13 AppleII::ProDOS::Directory->open($self->{disk}, $entry->block,
1001             $self->{bitmap});
1002             } # end AppleII::ProDOS::Directory::open_dir
1003              
1004             #---------------------------------------------------------------------
1005             # Add a new file to the directory:
1006             #
1007             # Input:
1008             # file: The AppleII::ProDOS::File to add
1009              
1010             sub put_file
1011             {
1012 2     2   6 my ($self, $file) = @_;
1013              
1014 2         4 eval {
1015 2         13 $file->allocate_space($self->{bitmap});
1016 2         15 $self->add_entry($file);
1017 2         8 $file->write_disk($self->{disk});
1018 2         11 $self->write_disk;
1019 2         9 $self->{bitmap}->write_disk;
1020             };
1021 2 50       12 if ($@) {
1022 0         0 my $error = $@;
1023             # Clean up after failure:
1024 0         0 $self->read_disk;
1025 0         0 $self->{bitmap}->read_disk;
1026 0         0 die $error;
1027             }
1028             } # end AppleII::ProDOS::Directory::put_file
1029              
1030             #---------------------------------------------------------------------
1031             # Read directory from disk:
1032              
1033             sub read_disk
1034             {
1035 3     3   7 my ($self, $block) = @_;
1036 3 50       9 $block = $self->{blocks}[0] unless $block;
1037              
1038 3         6 my (@blocks,@entries);
1039 3         12 my $disk = $self->{disk};
1040 3         5 my $entry = 0;
1041 3         10 while ($block) {
1042 6         23 push @blocks, $block;
1043 6         25 my $data = $disk->read_block($block);
1044 6         24 $block = unpack('v',substr($data,0x02,2)); # Pointer to next block
1045 6         13 substr($data,0,4) = ''; # Remove block pointers
1046 6         18 while ($data) {
1047 84         141 my ($type, $name) = parse_name($data);
1048 84 100       210 if (($type & 0xE) == 0xE) {
    100          
1049             # Directory header
1050 3         8 $self->{name} = $name;
1051 3         9 $self->{type} = $type;
1052 3         10 $self->{reserved} = substr($data, 0x14-4,8);
1053 3         8 $self->{created} = substr($data, 0x1C-4,4);
1054 3         12 $self->{version} = substr($data, 0x20-4,2);
1055 3         12 $self->{access} = ord substr($data, 0x22-4,1);
1056 3 100       17 if ($type == 0xE) {
1057             # For subdirectory, read parent pointers
1058 2         8 @{$self}{qw(parent parentNum)} =
  2         7  
1059             unpack('vC',substr($data,0x27-4,3));
1060             } # end if subdirectory
1061             } elsif ($type) {
1062             # File entry
1063 9         30 push @entries, AppleII::ProDOS::DirEntry->new($entry, $data);
1064             }
1065 84         108 substr($data,0,0x27) = ''; # Remove record
1066 84         166 ++$entry;
1067             } # end while more records
1068             } # end if rebuilding block list
1069              
1070 3         8 @{$self}{qw(blocks entries)} = (\@blocks, \@entries);
  3         15  
1071             } # end AppleII::ProDOS::Directory::read_disk
1072              
1073             #---------------------------------------------------------------------
1074             # Write directory to disk:
1075              
1076             sub write_disk
1077             {
1078 2     2   4 my ($self) = @_;
1079              
1080 2         6 my $disk = $self->{disk};
1081 2         4 my @blocks = @{$self->{blocks}};
  2         9  
1082 2         3 my @entries = @{$self->{'entries'}};
  2         9  
1083 2         5 my $keyBlock = $blocks[0];
1084              
1085 2 50       9 if ($self->{fixParent}) {
1086 0         0 delete $self->{fixParent};
1087 0         0 my $data = $disk->read_block($self->{parent});
1088 0         0 my $entry = 4 + 0x27*($self->{parentNum}-1);
1089 0         0 substr($data, $entry + 0x11, 7) =
1090             pack('v2VX', $keyBlock, scalar(@blocks), 0x200 * scalar(@blocks));
1091             # FIXME update modified date?
1092 0         0 $disk->write_block($self->{parent}, $data);
1093 0         0 my $parentBlock = unpack('v', substr($data,$entry + 0x25, 2));
1094 0 0       0 $disk->{os_openDirs}{$parentBlock}->read_disk
1095             if $disk->{os_openDirs}{$parentBlock};
1096             } # end if parent entry needs updating
1097              
1098 2         4 push @blocks, 0; # Add marker at beginning and end
1099 2         5 unshift @blocks, 0;
1100 2         3 my ($i, $entry);
1101 2         11 for ($i=1, $entry=0; $i < $#blocks; $i++) {
1102 2         10 my $data = pack('v2',$blocks[$i-1],$blocks[$i+1]); # Block pointers
1103 2         7 while (length($data) < 0x1FF) {
1104 26 100       43 if ($entry) {
1105             # Add a file entry:
1106 24 100 66     69 if (@entries and $entries[0]{num} == $entry) {
1107 7         28 $data .= $entries[0]->packed($keyBlock); shift @entries;
  7         9  
1108             } else {
1109 17         26 $data .= "\0" x 0x27;
1110             }
1111             } else {
1112             # Add the directory header:
1113 2         4 $data .= pack_name(@{$self}{'type','name'});
  2         10  
1114 2         8 $data .= $self->{reserved};
1115 2         4 $data .= $self->{created};
1116 2         5 $data .= $self->{version};
1117 2         6 $data .= chr $self->{access};
1118 2         5 $data .= "\x27\x0D"; # Entry length, entries per block
1119 2         5 $data .= pack('v',$#entries+1);
1120 2 50       8 if ($self->{type} == 0xF) {
1121 0         0 my $bitmap = $self->{bitmap};
1122 0         0 $data .= pack('v2',$bitmap->start_block,$bitmap->diskSize);
1123             } else {
1124 2         3 $data .= pack('vCC',@{$self}{'parent','parentNum'},
  2         8  
1125             0x27); # Parent entry length
1126             } # end else subdirectory
1127             } # end else if directory header
1128 26         76 ++$entry;
1129             } # end while more room in block
1130 2         13 $disk->write_block($blocks[$i],$data."\0");
1131             } # end for each directory block
1132             } # end AppleII::ProDOS::Directory::write_disk
1133              
1134             #=====================================================================
1135             package AppleII::ProDOS::DirEntry;
1136             #
1137             # Member Variables:
1138             # access: The access attributes
1139             # auxtype: The auxiliary type
1140             # block: The key block for this file
1141             # blksUsed: The number of blocks used by this file
1142             # created: The creation date/time
1143             # modified: The date/time of last modification
1144             # name: The filename
1145             # num: The entry number of this entry
1146             # size: The file size in bytes
1147             # storage: The storage type
1148             # type: The file type
1149             # version: The contents of the VERSION & MIN_VERSION (2 byte string)
1150             #---------------------------------------------------------------------
1151             AppleII::ProDOS->import(qw(pack_date pack_name parse_name parse_type
1152             valid_date valid_name));
1153 2     2   14 use integer;
  2         4  
  2         15  
1154 2     2   155 use bytes;
  2         4  
  2         10  
1155 2     2   38 use strict;
  2         3  
  2         55  
1156 2     2   16 use warnings;
  2         2  
  2         1297  
1157              
1158             our @ISA = 'AppleII::ProDOS::Members';
1159              
1160             my %de_fields = (
1161             access => 0xFF,
1162             auxtype => 0xFFFF,
1163             block => sub { not defined $_[0]{block} },
1164             blksUsed => sub { not defined $_[0]{blksUsed} },
1165             created => \&valid_date,
1166             modified => \&valid_date,
1167             name => \&valid_name,
1168             num => sub { not defined $_[0]{num} },
1169             size => sub { not defined $_[0]{size} },
1170             storage => sub { not defined $_[0]{storage} },
1171             type => 0xFF,
1172             );
1173              
1174             #---------------------------------------------------------------------
1175             # Constructor:
1176             #
1177             # Input:
1178             # number: The entry number
1179             # entry: The directory entry
1180              
1181             sub new
1182             {
1183 9     9   19 my ($type, $number, $entry) = @_;
1184 9         12 my $self = {};
1185              
1186 9         24 $self->{'_permitted'} = \%de_fields;
1187 9 50       21 if ($entry) {
1188 9         18 $self->{num} = $number;
1189 9         17 @{$self}{'storage', 'name'} = parse_name($entry);
  9         24  
1190 9         29 @{$self}{qw(type block blksUsed size)} = unpack('x16Cv2V',$entry);
  9         36  
1191 9         16 $self->{size} &= 0xFFFFFF; # Size is only 3 bytes long
1192 9         18 @{$self}{qw(access auxtype)} = unpack('x30Cv',$entry);
  9         20  
1193              
1194 9         21 $self->{created} = substr($entry,0x18,4);
1195 9         19 $self->{modified} = substr($entry,0x21,4);
1196 9         19 $self->{version} = substr($entry,0x1C,2);
1197             } else {
1198             # Blank entry:
1199 0         0 $self->{created} = $self->{modified} = pack_date(time);
1200 0         0 @{$self}{qw(access auxtype type version)} =
  0         0  
1201             (0xE3, 0x0000, 0x00, "\0\0");
1202             }
1203 9         32 bless $self, $type;
1204             } # end AppleII::ProDOS::DirEntry::new
1205              
1206             #---------------------------------------------------------------------
1207             # Return the entry as a packed string:
1208             #
1209             # Input:
1210             # keyBlock: The block number of the beginning of the directory
1211             #
1212             # Returns:
1213             # A directory entry ready to put in a ProDOS directory
1214              
1215             sub packed
1216             {
1217 7     7   11 my ($self, $keyBlock) = @_;
1218 7         9 my $data = pack_name(@{$self}{'storage', 'name'});
  7         102  
1219 7         12 $data .= pack('Cv2VX',@{$self}{qw(type block blksUsed size)});
  7         26  
1220 7         21 $data .= $self->{created} . $self->{version};
1221 7         9 $data .= pack('Cv',@{$self}{qw(access auxtype)});
  7         18  
1222 7         12 $data .= $self->{modified};
1223 7         21 $data .= pack('v',$keyBlock);
1224             } # end AppleII::ProDOS::DirEntry::packed
1225              
1226             #---------------------------------------------------------------------
1227             # Return the filetype as a string:
1228              
1229             sub short_type
1230             {
1231 12     12   40 parse_type(shift->{type});
1232             } # end AppleII::ProDOS::DirEntry::short_type
1233              
1234             #=====================================================================
1235             package AppleII::ProDOS::File;
1236             #
1237             # Member Variables:
1238             # data: The contents of the file
1239             # indexBlocks: For tree files, the number of subindex blocks needed
1240             #
1241             # Private Members (for communication between allocate_space & write_disk):
1242             # blocks: The list of data blocks allocated for this file
1243             # indexBlocks: For tree files, the list of subindex blocks
1244             #---------------------------------------------------------------------
1245              
1246             AppleII::ProDOS->import(qw(a2_croak valid_date valid_name));
1247 2     2   11 use Carp;
  2         4  
  2         115  
1248 2     2   9 use bytes;
  2         4  
  2         8  
1249 2     2   126 use strict;
  2         4  
  2         67  
1250 2     2   178 use warnings;
  2         3  
  2         2806  
1251              
1252             our @ISA = 'AppleII::ProDOS::DirEntry';
1253              
1254             my %fil_fields = (
1255             access => 0xFF,
1256             auxtype => 0xFFFF,
1257             blksUsed => undef,
1258             created => \&valid_date,
1259             data => undef,
1260             modified => \&valid_date,
1261             name => \&valid_name,
1262             size => undef,
1263             type => 0xFF,
1264             );
1265              
1266             #---------------------------------------------------------------------
1267             # Constructor for creating a new file:
1268             #
1269             # Input:
1270             # name: The filename
1271             # data: The contents of the file
1272              
1273             sub new
1274             {
1275 2     2   7 my ($type, $name, $data) = @_;
1276 2 50       8 a2_croak("Invalid name `$name'") unless valid_name($name);
1277              
1278 2         28 my $self = {
1279             access => 0xE3,
1280             auxtype => 0,
1281             created => "\0\0\0\0",
1282             data => $data,
1283             modified => "\0\0\0\0",
1284             name => uc $name,
1285             size => length($data),
1286             type => 0,
1287             version => "\0\0",
1288             _permitted => \%fil_fields
1289             };
1290              
1291 2         13 bless $self, $type;
1292             } # end AppleII::ProDOS::File::new
1293              
1294             #---------------------------------------------------------------------
1295             # Open a file:
1296             #
1297             # Input:
1298             # disk: The disk to read
1299             # entry: The AppleII::ProDOS::DirEntry that describes the file
1300              
1301             sub open
1302             {
1303 10     10   23 my ($type, $disk, $entry) = @_;
1304 10         35 my $self = { _permitted => \%fil_fields };
1305 10         42 my @fields = qw(access auxtype blksUsed created modified name size
1306             storage type version);
1307 10         20 @{$self}{@fields} = @{$entry}{@fields};
  10         89  
  10         48  
1308              
1309 10         29 my ($storage, $keyBlock, $size) =
1310 10         24 @{$entry}{qw(storage block size)};
1311              
1312 10         15 my $data;
1313 10 100       35 if ($storage == 1) {
1314 2         12 $data = $disk->read_block($keyBlock);
1315             } else {
1316             # Calculate the number of data blocks:
1317             # (In a sparse file, not all these blocks
1318             # are actually allocated.)
1319 8         20 my $blksUsed = int(($size + 0x1FF) / 0x200);
1320              
1321 8 100       28 if ($storage == 2) {
    50          
1322 5         31 my $index = AppleII::ProDOS::Index->open($disk,$keyBlock,$blksUsed);
1323 5         38 $data = $disk->read_blocks($index->blocks);
1324             } elsif ($storage == 3) {
1325 3         9 my $indexBlocks = int(($blksUsed + 0xFF) / 0x100);
1326 3         24 my $index = AppleII::ProDOS::Index->open($disk,$keyBlock,$indexBlocks);
1327 3         6 my (@blocks,$block);
1328 3         9 foreach $block (@{$index->blocks}) {
  3         31  
1329 6 50       22 if ($block) {
1330 6         21 my $subindex = AppleII::ProDOS::Index->open($disk,$block);
1331 6         10 push @blocks,@{$subindex->blocks};
  6         33  
1332             } else {
1333 0         0 push @blocks, (0) x 0x100; # Sparse index block
1334             }
1335             } # end foreach subindex block
1336 3         31 $#blocks = $blksUsed-1; # Use only the first $blksUsed blocks
1337 3         19 $data = $disk->read_blocks(\@blocks);
1338 3         49 $self->{indexBlocks} = $indexBlocks;
1339             } else {
1340 0         0 croak("Unsupported storage type $storage");
1341             }
1342             } # end else not a seedling file
1343              
1344 10 100       60 substr($data, $size) = '' if length($data) > $size;
1345 10         695 $self->{'data'} = $data;
1346              
1347 10         87 bless $self, $type;
1348             } # end AppleII::ProDOS::File::open
1349              
1350             #---------------------------------------------------------------------
1351             # Allocate space for the file:
1352             #
1353             # Input:
1354             # bitmap: The AppleII::ProDOS::Bitmap we should use
1355             #
1356             # Input Variables:
1357             # data: The data we're trying to store
1358             #
1359             # Output Variables:
1360             # blksUsed: The number of blocks used by the file (including indexes)
1361             # blocks: The list of data blocks allocated
1362             # indexBlocks: The list of subindex blocks allocated
1363             # storage: The storage type of the file
1364              
1365             sub allocate_space
1366             {
1367 2     2   5 my ($self, $bitmap) = @_;
1368              
1369             # Decide which storage type this file requires:
1370 2         7 my $dataRef = \$self->{data};
1371              
1372 2         34 my @dataBlks = (1) x int((length($$dataRef) + 0x1FF) / 0x200);
1373 2         4 my @subindexBlks;
1374             my $storage;
1375              
1376 2 100       11 if (@dataBlks > 0x100) {
    50          
1377 1         4 $storage = 3; # > 128KB = Tree
1378 1         5 @subindexBlks = (1) x int((@dataBlks + 0xFF) / 0x100);
1379             } elsif (@dataBlks > 1) {
1380 1         3 $storage = 2; # 513 bytes - 128KB = Sapling
1381             } else {
1382 0         0 $storage = 1; # 0 - 512 bytes = Seedling
1383 0         0 @dataBlks = (1); # Even empty files need one block
1384             }
1385              
1386             # Calculate how many blocks the file will occupy:
1387 2         5 my $blksUsed = scalar @dataBlks;
1388              
1389 2 50       8 if ($storage > 1) {
1390 2         74 $blksUsed += 1 + @subindexBlks; # Add in the index blocks
1391              
1392             # Check to see if this file is sparse:
1393 2         4 my $index = 0;
1394 2         4 foreach (@dataBlks) {
1395 266 100       5130 unless (substr($$dataRef, $index, 0x200) =~ /[^\0]/) {
1396 257         275 $_ = 0; # This data block doesn't need to be allocated
1397 257         330 --$blksUsed;
1398             } # end unless this block contains data
1399 266         885 $index += 0x200; # 512 bytes per data block
1400             } # end foreach data block
1401              
1402             # For tree files, figure out which subindex blocks are needed:
1403 2 100       19 if (@subindexBlks) {
1404 1         110 my @blocks = @dataBlks;
1405 1         4 foreach my $ib (@subindexBlks) {
1406 2 50       11 unless (grep { $_ } splice @blocks, 0, 0x100) {
  259         282  
1407 0         0 $ib = 0; # This subindex block doesn't need to be allocated
1408 0         0 --$blksUsed;
1409             } # end unless this subindex block is required
1410             } # end foreach subindex block
1411             } # end if tree file
1412             } # end if not seedling
1413              
1414 2         7 $self->{storage} = $storage;
1415 2         6 $self->{blksUsed} = $blksUsed;
1416              
1417             # Now allocate the blocks and record them:
1418 2 50       13 my @blocks = $bitmap->get_blocks($blksUsed)
1419             or a2_croak("Not enough free space");
1420              
1421 2         5 $self->{block} = $blocks[0];
1422              
1423 2 50       7 shift @blocks if $storage > 1; # Remove index block from list
1424              
1425 2         6 foreach (@subindexBlks, @dataBlks) {
1426             # If this block needs to be allocated, assign it one of our blocks:
1427 268 100       460 $_ = shift @blocks if $_;
1428             }
1429              
1430 2 100       8 if ($storage == 3) {
1431 1         5 $self->{indexBlocks} = \@subindexBlks;
1432             } else {
1433 1         2 delete $self->{indexBlocks}; # Just in case
1434             }
1435              
1436 2         88 $self->{blocks} = \@dataBlks;
1437             } # end AppleII::ProDOS::File::allocate_space
1438              
1439             #---------------------------------------------------------------------
1440             # Return the file's contents as text:
1441             #
1442             # Returns:
1443             # The file's contents with hi bits stripped and CRs converted to \n
1444              
1445             sub as_text
1446             {
1447 9     9   9511 my $self = shift;
1448 9         84 my $data = $self->{data};
1449 9         8496 $data =~ tr/\x0D\x8D\x80-\xFF/\n\n\x00-\x7F/;
1450 9         1373 $data;
1451             } # end AppleII::ProDOS::File::as_text
1452              
1453             #---------------------------------------------------------------------
1454             # Write the file to disk:
1455             #
1456             # You must have already called allocate_space.
1457             #
1458             # Input:
1459             # disk: The disk to write to
1460             #
1461             # Input Variables:
1462             # blocks: The list of data blocks allocated
1463             # indexBlocks: The list of subindex blocks allocated
1464             #
1465             # Output Variables:
1466             # indexBlocks: The number of subindex blocks needed
1467              
1468             sub write_disk
1469             {
1470 2     2   4 my ($self, $disk) = @_;
1471              
1472 2         21 $disk->write_blocks($self->{blocks}, $self->{'data'}, "\0");
1473              
1474 2         7 my $storage = $self->{storage};
1475 2 100       13 if ($storage == 2) {
    50          
1476 1         6 my $index = AppleII::ProDOS::Index->new($disk,
1477 1         1 @{$self}{qw(block blocks)});
1478 1         4 $index->write_disk;
1479             } elsif ($storage == 3) {
1480 1         12 my $index =
1481 1         4 AppleII::ProDOS::Index->new($disk, @{$self}{qw(block indexBlocks)});
1482 1         6 $index->write_disk;
1483 1         3 my @blocks = @{$self->{blocks}};
  1         23  
1484 1         2 my $block;
1485 1         4 foreach $block (@{$self->{indexBlocks}}) {
  1         4  
1486 2 50       9 if ($block) {
1487 2         26 $index = AppleII::ProDOS::Index->new($disk, $block,
1488             [splice(@blocks,0,0x100)]);
1489 2         18 $index->write_disk;
1490             } else {
1491 0         0 splice(@blocks,0,0x100);
1492             } # end else sparse index block is not actually allocated
1493             } # end for each subindex block
1494 1         3 $self->{indexBlocks} = scalar @{$self->{indexBlocks}};
  1         6  
1495             } # end elsif tree file
1496              
1497 2         16 delete $self->{blocks};
1498             } # end AppleII::ProDOS::File::write_disk
1499              
1500             #=====================================================================
1501             package AppleII::ProDOS::Index;
1502             #
1503             # Member Variables:
1504             # block: The block number of the index block
1505             # blocks: The list of blocks pointed to by this index block
1506             # disk: An AppleII::Disk
1507             #---------------------------------------------------------------------
1508              
1509 2     2   15 use integer;
  2         5  
  2         16  
1510 2     2   49 use bytes;
  2         4  
  2         10  
1511 2     2   44 use strict;
  2         4  
  2         56  
1512 2     2   15 use warnings;
  2         3  
  2         1034  
1513              
1514             our @ISA = 'AppleII::ProDOS::Members';
1515              
1516             my %in_fields = (
1517             blocks => undef,
1518             );
1519              
1520             #---------------------------------------------------------------------
1521             # Constructor for creating a new index block:
1522             #
1523             # Input:
1524             # disk: An AppleII::Disk
1525             # block: The block number of the index block
1526             # blocks: The list of blocks that are pointed to by this block
1527              
1528             sub new
1529             {
1530 4     4   8 my ($type, $disk, $block, $blocks) = @_;
1531 4         22 my $self = {
1532             disk => $disk,
1533             block => $block,
1534             blocks => $blocks,
1535             _permitted => \%in_fields,
1536             };
1537              
1538 4         19 bless $self, $type;
1539             } # end AppleII::ProDOS::Index::new
1540              
1541             #---------------------------------------------------------------------
1542             # Constructor for reading an existing index block:
1543             #
1544             # Input:
1545             # disk: An AppleII::Disk
1546             # block: The block number to read
1547             # count: The number of blocks that are pointed to by this block
1548             # (optional; default is 256)
1549              
1550             sub open
1551             {
1552 14     14   32 my ($type, $disk, $block, $count) = @_;
1553 14         26 my $self = {};
1554 14         36 $self->{disk} = $disk;
1555 14         26 $self->{block} = $block;
1556 14         30 $self->{'_permitted'} = \%in_fields;
1557              
1558 14         44 bless $self, $type;
1559 14         38 $self->read_disk($count);
1560 14         39 $self;
1561             } # end AppleII::ProDOS::Index::open
1562              
1563             #---------------------------------------------------------------------
1564             # Read contents of index block from disk:
1565             #
1566             # Input:
1567             # count:
1568             # The number of blocks that are pointed to by this block
1569             # (optional; default is 256)
1570              
1571             sub read_disk
1572             {
1573 14     14   25 my ($self, $count) = @_;
1574 14 100       37 $count = 0x100 unless $count;
1575 14         82 my @dataLo = unpack('C*',$self->{disk}->read_block($self->{block}));
1576 14         667 my @dataHi = splice @dataLo, 0x100;
1577 14         117 my @blocks;
1578              
1579 14         41 while (--$count >= 0) {
1580 1809         9606 push @blocks, shift(@dataLo) + 0x100 * shift(@dataHi);
1581             }
1582              
1583 14         133 $self->{blocks} = \@blocks;
1584             } # end AppleII::ProDOS::Index::read_disk
1585              
1586             #---------------------------------------------------------------------
1587             # Write index block to disk:
1588              
1589             sub write_disk
1590             {
1591 4     4   9 my $self = shift;
1592 4         9 my $disk = $self->{disk};
1593              
1594 4         4 my ($dataLo, $dataHi);
1595 4         6 $dataLo = $dataHi = pack('v*',@{$self->{blocks}});
  4         24  
1596 4         164 $dataLo =~ s/(.)./$1/gs; # Keep just the low byte
1597 4         143 $dataHi =~ s/.(.)/$1/gs; # Keep just the high byte
1598              
1599 4         15 $disk->write_block($self->{block},
1600             AppleII::Disk::pad_block($dataLo,"\0",0x100) . $dataHi,
1601             "\0");
1602             } # end AppleII::ProDOS::Index::write_disk
1603              
1604             #=====================================================================
1605             package AppleII::ProDOS::Members;
1606             #
1607             # Provides access functions for member variables. This class is based
1608             # on code from Tom Christiansen's FMTEYEWTK on OO Perl vs. C++.
1609             #
1610             # Only those member variables whose names are listed in the _permitted
1611             # hash may be accessed.
1612             #
1613             # The value in the _permitted hash is used for validating the new
1614             # value of a field. The possible values are:
1615             # undef No changes allowed (read-only)
1616             # CODE ref Call CODE with our @_. It returns true if OK.
1617             # scalar New value must be an integer between 0 and _permitted
1618             #---------------------------------------------------------------------
1619              
1620 2     2   12 use Carp;
  2         4  
  2         971  
1621              
1622             sub AUTOLOAD
1623             {
1624 150     150   2114 my $self = $_[0];
1625 150 50       367 my $type = ref($self) or croak("$self is not an object");
1626 150         276 my $name = our $AUTOLOAD;
1627 150         761 $name =~ s/.*://; # strip fully-qualified portion
1628 150         405 my $field = $name;
1629 150         324 $field =~ s/_([a-z])/\u$1/g; # squash underlines into mixed case
1630 150 50       475 unless (exists $self->{'_permitted'}{$field}) {
1631             # Ignore special methods like DESTROY:
1632 0 0       0 return undef if $name =~ /^[A-Z]+$/;
1633 0         0 croak("Can't access `$name' field in object of class $type");
1634             }
1635 150 100       322 if ($#_) {
1636 2         5 my $check = $self->{'_permitted'}{$field};
1637 2         4 my $ok;
1638 2 50       7 if (ref($check) eq 'CODE') {
    0          
1639 2         4 $ok = &$check; # Pass our @_ to validator
1640             } elsif ($check) {
1641 0   0     0 $ok = ($_[1] =~ /^[0-9]+$/ and $_[1] >= 0 and $_[1] <= $check);
1642             } else {
1643 0         0 croak("Field `$name' of class $type is read-only");
1644             }
1645 2 50       9 return $self->{$field} = $_[1] if $ok;
1646 0         0 croak("Invalid value `$_[1]' for field `$name' of class $type");
1647             }
1648 148         1051 return $self->{$field};
1649             } # end AppleII::ProDOS::Members::AUTOLOAD
1650              
1651             #=====================================================================
1652             # Package Return Value:
1653              
1654             1;
1655              
1656             __END__