File Coverage

blib/lib/AppleII/ProDOS.pm
Criterion Covered Total %
statement 449 578 77.6
branch 84 166 50.6
condition 14 50 28.0
subroutine 68 83 81.9
pod 4 15 26.6
total 619 892 69.3


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