File Coverage

blib/lib/Mac/AppleSingleDouble.pm
Criterion Covered Total %
statement 133 206 64.5
branch 19 34 55.8
condition n/a
subroutine 20 28 71.4
pod 17 17 100.0
total 189 285 66.3


line stmt bran cond sub pod time code
1             # Mac::AppleSingleDouble.pm, (C) 2001 Jamie Flournoy (jamie@white-mountain.org).
2              
3             package Mac::AppleSingleDouble;
4             require 5;
5 13     13   157575 use FileHandle;
  13         268829  
  13         90  
6              
7             $Mac::AppleSingleDouble::VERSION='1.0';
8              
9             # default Finder colors for label values.
10             %labelcolors = (0 => 'Black',
11             1 => 'Brown',
12             2 => 'Green',
13             3 => 'Blue',
14             4 => 'Cyan',
15             5 => 'Pink',
16             6 => 'Red',
17             7 => 'Orange');
18              
19             # default Finder names for label values.
20             %labelnames = (0 => 'None',
21             1 => 'Project 2',
22             2 => 'Project 1',
23             3 => 'Personal',
24             4 => 'Cool',
25             5 => 'In Progress',
26             6 => 'Hot',
27             7 => 'Essential');
28              
29             %entryids = (1 => 'Data Fork',
30             2 => 'Resource Fork',
31             3 => 'Real Name',
32             4 => 'Comment',
33             5 => 'Icon, B&W',
34             6 => 'Icon, Color',
35             8 => 'File Dates Info',
36             9 => 'Finder Info',
37             10 => 'Macintosh File Info',
38             11 => 'ProDOS File Info',
39             12 => 'MS-DOS File Info',
40             13 => 'Short Name',
41             14 => 'AFP File Info',
42             15 => 'Directory ID');
43              
44             # Magic number values mapped to file format (AppleSingle or
45             # AppleDouble). Any other value means it's not an AppleSingle or
46             # AppleDouble file.
47             %formats = ( pack('H8', "00051600") => 'AppleSingle',
48             pack('H8', "00051607") => 'AppleDouble');
49              
50             sub new
51             {
52 13     13 1 182 my $class = shift;
53 13         41 my $filename = shift;
54 13 100       66 if (!defined($filename))
55             {
56 1         13 die "The constructor (new) requires a filename as an argument!";
57             }
58 12         32 my $this = {}; # instances are based on hashes
59 12         39 bless $this, $class; # now $this is an instance of $class
60 12         72 $this->_initialize($filename);
61 10         35 return $this;
62             }
63              
64             sub DESTROY
65             {
66 11     11   128 my $this = shift;
67 11         36 $this->close();
68             }
69              
70             sub close
71             {
72 20     20 1 3823 my $this = shift;
73 20 100       1092 if ($this->{'_filehandle'})
74             {
75 8         122 CORE::close $this->{'_filehandle'};
76 8         64 undef($this->{'_filehandle'});
77             }
78             }
79              
80             sub get_finder_info
81             {
82 3     3 1 20 my $this = shift;
83 3         21 $this->_require_applesingledouble();
84 3 50       13 if (!defined($this->{'_finder_info'}))
85             {
86 3         41 $this->_parse_finder_info($this->get_entry(9));
87             }
88 3         12 return $this->{'_finder_info'}
89             }
90              
91             sub get_entry
92             {
93 5     5 1 18 my $this = shift;
94 5         10 my $entryid = shift;
95 5         20 $this->_require_applesingledouble();
96 5         15 my $entry = $this->{'_entries'}->{$entryid};
97 5 50       16 if (!defined($entry))
98             {
99 5         17 $entry = $this->_get_entry_from_file($entryid);
100             }
101 5 50       17 if ($this->{'_cache_entries'})
102             {
103 0         0 $this->{'_entries'}->{$entryid} = $entry;
104             }
105 5         18 return $entry;
106             }
107              
108             sub get_file_format
109             {
110 26     26 1 49 my $this = shift;
111 26 100       107 if (!defined($this->{'_magicno'}))
112             {
113 9         50 $this->_parse_header();
114             }
115 26         59 my $format = $formats{$this->{'_magicno'}};
116 26 100       62 if (!defined($format)) { $format = 'Plain'; }
  2         3  
117 26         108 return $format;
118             }
119              
120             sub is_applesingle
121             {
122 1     1 1 8 my $this = shift;
123 1         7 return ($this->get_file_format() eq 'AppleSingle');
124             }
125              
126             sub is_appledouble
127             {
128 1     1 1 8 my $this = shift;
129 1         6 return $this->get_file_format() eq 'AppleDouble';
130             }
131              
132             sub preload_entire_file
133             {
134 0     0 1 0 my $this = shift;
135 0         0 $this->_require_applesingledouble();
136 0         0 $this->cache_entries(1);
137 0         0 $this->get_all_entries();
138 0         0 $this->close();
139             }
140              
141             sub cache_entries
142             {
143 0     0 1 0 my $this = shift;
144 0         0 my $val = shift;
145 0 0       0 if (defined($val))
146             {
147 0         0 $this->{'_cache_entries'} = $val;
148             }
149 0         0 return $this->{'_cache_entries'};
150             }
151              
152              
153             sub get_entry_descriptors
154             {
155 5     5 1 7 my $this = shift;
156 5         12 $this->_require_applesingledouble();
157 5         11 return $this->{'_descriptors'};
158             }
159              
160             sub get_all_entries
161             {
162 0     0 1 0 my $this = shift;
163 0         0 $this->_require_applesingledouble();
164 0         0 my %entries = ();
165 0         0 my $descriptors = $this->get_entry_descriptors();
166 0         0 foreach $entryid (keys( %{$descriptors} ))
  0         0  
167             {
168 0         0 $entries{$entryid} = $this->get_entry($entryid);
169             }
170 0         0 return \%entries;
171             }
172              
173             sub set_labelnames
174             {
175 1     1 1 17 my $this = shift;
176 1         3 my $new_labelnames = shift;
177 1         4 $this->{'_labelnames'} = $new_labelnames;
178             }
179              
180             sub set_labelcolors
181             {
182 1     1 1 20 my $this = shift;
183 1         3 my $new_labelcolors = shift;
184 1         3 $this->{'_labelcolors'} = $new_labelcolors;
185             }
186              
187             sub dump
188             {
189 0     0 1 0 my $this = shift;
190 0         0 $this->dump_header();
191 0         0 print "\n";
192 0         0 $this->dump_entries();
193             }
194              
195             sub dump_header
196             {
197 0     0 1 0 my $this = shift;
198 0         0 $this->_require_applesingledouble();
199 0         0 print "Dumping " . $this->get_file_format() . " file '" . $this->{'_filename'} . "':\n";
200 0 0       0 if ($this->get_file_format() eq 'Plain')
201             {
202 0         0 print "Can't dump a file unless it's in AppleSingle or AppleDouble format.\n";
203 0         0 return;
204             }
205 0         0 print "File is " . $this->{'_size'} . " bytes long.\n";
206 0         0 print "Magic Number is " . unpack('H8', $this->{'_magicno'}) . ".\n";
207 0         0 print "Version Number is " . unpack('H8', $this->{'_version'}) . ".\n";
208              
209 0         0 print "Entry descriptor table:\n";
210 0         0 my $descriptors = $this->{'_descriptors'};
211 0         0 my $d = $descriptors; # make next line look purty
212 0         0 foreach $entryid (sort {$d->{$a}->{'Offset'} <=> $d->{$b}->{'Offset'} } keys( %{$descriptors} ))
  0         0  
  0         0  
213             {
214 0         0 print "Offset: " . $descriptors->{$entryid}->{'Offset'} . "\t";
215 0         0 print "Length: " . $descriptors->{$entryid}->{'Length'} . "\t";
216 0         0 my $entryidname = $entryids{$entryid};
217 0 0       0 if (!defined($entryidname)) { $entryidname = '???'; }
  0         0  
218 0         0 print "EntryID: $entryid ($entryidname)\n";
219             }
220             }
221              
222             sub dump_entries
223             {
224 0     0 1 0 my $this = shift;
225              
226 0         0 my $descriptors = $this->{'_descriptors'};
227 0         0 foreach $entryid (sort {$descriptors->{$a}->{'Offset'} <=> $descriptors->{$b}->{'Offset'} } keys( %{$descriptors} ))
  0         0  
  0         0  
228             {
229 0         0 $this->dump_entry($entryid);
230             }
231             }
232              
233             sub dump_entry
234             {
235 0     0 1 0 my $this = shift;
236 0         0 my $entryid = shift;
237              
238 0         0 my $entryidname = $entryids{$entryid};
239 0 0       0 if (!defined($entryidname)) { $entryidname = '???'; }
  0         0  
240 0         0 print "EntryID: $entryid ($entryidname)\n";
241 0         0 print $this->_hex_dump($this->get_entry($entryid)) . "\n";
242             }
243              
244             sub _hex_dump
245             {
246 0     0   0 my $this = shift;
247 0         0 my $bytes = shift;
248              
249 0         0 my $length = length($bytes);
250 0         0 my $hexdump = '';
251             # this code is based on a script by David Thorburn-Gundlach
252 0         0 for ($p = 0; $p < $length; $p += 16)
253             {
254 0         0 $byteno = sprintf('%8lx', $p);
255 0         0 $byteno =~ s/ /0/g;
256 0         0 $byteno =~ s/^(....)/$1 /g;
257 0         0 $asc_string = substr($bytes, $p, 16);
258 0         0 $hex_string = unpack('H32', $asc_string);
259 0         0 $hex_string =~ s/(..)/$1 /g;
260 0         0 $pad = ' ' x (3*(16-length($asc_string)));
261 0         0 $asc_string =~ s/([\00-\37,\177])/./g;
262 0         0 $hexdump .= "$byteno: $hex_string$pad $asc_string\n";
263             }
264 0         0 return $hexdump;
265             }
266              
267             sub _initialize
268             {
269 12     12   30 my $this = shift;
270 12         91 $this->{'_filename'} = shift;
271 12 100       391 if (!-f $this->{'_filename'})
272             {
273 2         31 die "'$this->{'_filename'}' is not a file!";
274             }
275 10         35 $this->{'_entries'} = {};
276 10         39 $this->{'_labelnames'} = \%labelnames;
277 10         42 $this->{'_labelcolors'} = \%labelcolors;
278             }
279              
280             sub _read_header
281             {
282 9     9   1202 my $this = shift;
283 9         3825 my $header_raw;
284              
285 9         105 my $fh = new FileHandle;
286 9         1025 $fh->open($this->{'_filename'});
287 9         718 ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks) = stat($fh);
288 9         609 $this->{'_size'} = $size;
289 9         169 read($fh, $header_raw, 26);
290             # not closed here - must use $this->close() later;
291 9         23 $this->{'_filehandle'} = $fh;
292              
293 9         26 return $header_raw;
294             }
295              
296             sub _parse_header
297             {
298 9     9   728 my $this = shift;
299 9 50       649 return if defined($this->{'_magicno'}); # already did it
300            
301 9         41 $header_raw = $this->_read_header();
302            
303 9         675 $this->{'_magicno'} = substr($header_raw, 0,4);
304 9 100       51 if ($this->get_file_format() ne 'Plain') # will not infinitely recurse because we just set _magicno
305             {
306 8         32 $this->{'_version'} = substr($header_raw, 4,4);
307 8         49 my $entrycount = unpack('n', substr($header_raw, 24,2));
308 8         31 my $descriptors_raw = $this->_read_descriptors($entrycount);
309 8         35 $this->_parse_descriptors($entrycount, $descriptors_raw);
310             }
311             }
312              
313             sub _read_descriptors
314             {
315 8     8   15 my $this = shift;
316 8         14 my $entrycount = shift;
317 8         11 my $descriptors_raw;
318             # this must be called after _read_header!
319 8         82 seek($this->{'_filehandle'}, 26, 0);
320 8         86 read($this->{'_filehandle'}, $descriptors_raw, $entrycount * 12);
321 8         50 return $descriptors_raw;
322             }
323              
324             sub _parse_descriptors
325             {
326 8     8   42 my $this = shift;
327 8         27 my $entrycount = shift;
328 8         12 my $descriptors_raw = shift;
329 8         38 my(%descriptors);
330              
331 8         35 for($i = 0; $i < $entrycount; $i++)
332             {
333 40         93 my(%descriptor);
334 40         50 $entrystart = (12 * $i);
335 40         113 $descriptor{'EntryID'} = unpack('N', substr($descriptors_raw, $entrystart, 4));
336 40         84 $descriptor{'Offset'} = unpack('N', substr($descriptors_raw, $entrystart + 4, 4));
337 40         74 $descriptor{'Length'} = unpack('N', substr($descriptors_raw, $entrystart + 8, 4));
338              
339             # store in the descriptors hash keyed by entry ID
340 40         144 $descriptors{$descriptor{'EntryID'}} = \%descriptor;
341             }
342              
343 8         26 $this->{'_descriptors'} = \%descriptors;
344             }
345              
346             sub _parse_finder_info
347             {
348 3     3   5 my $this = shift;
349 3         3 my $finderinfo_raw = shift;
350 3         4 my(%finderinfo);
351              
352             # based on page 7-76 of Inside Macintosh: Finder Interface
353 3         9 $finderinfo{'Type'} = substr($finderinfo_raw, 0, 4);
354 3         8 $finderinfo{'Creator'} = substr($finderinfo_raw, 4, 4);
355 3         9 $finderinfo{'Flags'} = unpack('n', substr($finderinfo_raw, 8, 2));
356 3         8 $finderinfo{'Location'} = unpack('nn', substr($finderinfo_raw, 10, 4));
357 3         9 $finderinfo{'Fldr'} = unpack('n', substr($finderinfo_raw, 14, 2));
358              
359             # Finder Flags
360 3         14 $flagbits = unpack('B8', substr($finderinfo_raw, 8, 1)) .unpack('B8', substr($finderinfo_raw, 9, 1)) ;
361             #print "flagbits is $flagbits\n";
362 3         22 $finderinfo{'Label'} = unpack('C', pack('B8', '0'x5 . substr($flagbits, 12, 3)));
363 3         6 $finderinfo{'Color'} = $finderinfo{'Label'};
364 3         10 $finderinfo{'IsOnDesk'} = substr($flagbits, 15, 1);
365 3         7 $finderinfo{'IsShared'} = substr($flagbits, 9, 1);
366 3         8 $finderinfo{'HasBeenInited'} = substr($flagbits, 7, 1);
367 3         7 $finderinfo{'HasCustomIcon'} = substr($flagbits, 5, 1);
368 3         7 $finderinfo{'IsStationery'} = substr($flagbits, 4, 1);
369 3         16 $finderinfo{'NameLocked'} = substr($flagbits, 3, 1);
370 3         15 $finderinfo{'HasBundle'} = substr($flagbits, 2, 1);
371 3         7 $finderinfo{'IsInvisible'} = substr($flagbits, 1, 1);
372 3         11 $finderinfo{'IsAlias'} = substr($flagbits, 0, 1);
373              
374             # Extended Finder Info
375 3         10 $finderinfo{'IconID'} = unpack('n', substr($finderinfo_raw, 16, 2));
376 3         7 $finderinfo{'Script'} = unpack('c', substr($finderinfo_raw, 24, 1));
377 3         20 $finderinfo{'XFlags'} = unpack('B8', substr($finderinfo_raw, 25, 1));
378 3         8 $finderinfo{'Comment'} = unpack('n', substr($finderinfo_raw, 26, 2));
379 3         8 $finderinfo{'PutAway'} = unpack('N', substr($finderinfo_raw, 28, 4));
380              
381 3         9 my $labelcolor = $this->{'_labelcolors'}->{$finderinfo{'Label'}};
382 3         10 my $labelname = $this->{'_labelnames'}->{$finderinfo{'Label'}};
383 3 50       11 $finderinfo{'LabelColor'} = defined($labelcolor)? $labelcolor : '(no labelcolor provided)';
384 3 50       10 $finderinfo{'LabelName'} = defined($labelname)? $labelname : '(no labelname provided)';
385              
386 3         8 $this->{'_finder_info'} = \%finderinfo;
387             }
388              
389             sub _get_entry_from_file
390             {
391 5     5   9 my $this = shift;
392 5         9 my $entryid = shift;
393 5         19 my $descriptors = $this->get_entry_descriptors();
394            
395 5         10 my $descriptor = $descriptors->{$entryid};
396 5         7 my $entry;
397 5         42 seek($this->{'_filehandle'}, $descriptor->{'Offset'}, 0);
398 5         34 read($this->{'_filehandle'}, $entry, $descriptor->{'Length'});
399 5         15 return $entry;
400             }
401              
402             sub _require_applesingledouble
403             {
404 13     13   26 my $this = shift;
405 13 50       43 if ($this->get_file_format() eq 'Plain')
406             {
407 0           die "File '" . $this->{'_filename'} . "' is not in AppleSingle or AppleDouble format!";
408             }
409             }
410              
411             1;
412             __END__