File Coverage

blib/lib/D64/Disk/Dir.pm
Criterion Covered Total %
statement 198 208 95.1
branch 34 46 73.9
condition 6 12 50.0
subroutine 31 31 100.0
pod 9 9 100.0
total 278 306 90.8


line stmt bran cond sub pod time code
1             package D64::Disk::Dir;
2              
3             =head1 NAME
4              
5             D64::Disk::Dir - Handling entire Commodore (D64/D71/D81) disk image directories (using Per Olofsson's "diskimage.c" library)
6              
7             =head1 SYNOPSIS
8              
9             use D64::Disk::Dir;
10              
11             # Read entire D64/D71/D81 disk image directory from file on disk in one step:
12             my $d64DiskDirObj = D64::Disk::Dir->new($filename);
13              
14             # Read entire D64/D71/D81 disk image directory from file on disk in two steps:
15             my $d64DiskDirObj = D64::Disk::Dir->new();
16             my $readOK = $d64DiskDirObj->read_dir($filename);
17              
18             # Read new D64/D71/D81 disk directory replacing currently loaded dir with it:
19             my $readOK = $d64DiskDirObj->read_dir($filename);
20              
21             # Get disk directory title converted to ASCII string:
22             my $convert2ascii = 1;
23             my $title = $d64DiskDirObj->get_title($convert2ascii);
24              
25             # Get disk directory ID converted to ASCII string:
26             my $convert2ascii = 1;
27             my $diskID = $d64DiskDirObj->get_id($convert2ascii);
28              
29             # Get number of blocks free:
30             my $blocksFree = $d64DiskDirObj->get_blocks_free();
31              
32             # Get number of directory entries:
33             my $num_entries = $d64DiskDirObj->num_entries();
34              
35             # Get directory entry at the specified position:
36             my $entryObj = $d64DiskDirObj->get_entry($index);
37              
38             # Get binary file data from a directory entry at the specified position:
39             my $data = $d64DiskDirObj->get_file_data($index);
40              
41             # Print out the entire directory content to the standard output:
42             $d64DiskDirObj->print_dir();
43              
44             =head1 DESCRIPTION
45              
46             This package provides an abstract layer above D64::Disk::Image module, enabling user to handle D64 disk image directories in a higher-level object-oriented way.
47              
48             =head1 METHODS
49              
50             =cut
51              
52 4     4   417859 use bytes;
  4         90  
  4         24  
53 4     4   121 use strict;
  4         18  
  4         94  
54 4     4   19 use warnings;
  4         22  
  4         117  
55              
56 4     4   31 use base qw( Exporter );
  4         7  
  4         1042  
57             our %EXPORT_TAGS = ();
58             $EXPORT_TAGS{'all'} = [];
59             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
60             our @EXPORT = qw();
61              
62             our $VERSION = '0.05';
63              
64 4     4   29 use Carp qw/carp croak verbose/;
  4         8  
  4         629  
65 4     4   2044 use Data::Dumper;
  4         21802  
  4         265  
66 4     4   1077 use IO::Scalar;
  4         26218  
  4         189  
67 4     4   2939 use Term::ANSIColor qw(:constants);
  4         38288  
  4         5646  
68              
69 4     4   2178 use D64::Disk::Dir::Entry;
  4         11  
  4         130  
70 4     4   62 use D64::Disk::Image qw(:all);
  4         16  
  4         8353  
71              
72             # Mapping file types onto file type constants:
73             our %file_type_constants = (
74             'del' => T_DEL,
75             'seq' => T_SEQ,
76             'prg' => T_PRG,
77             'usr' => T_USR,
78             'rel' => T_REL,
79             'cbm' => T_CBM,
80             'dir' => T_DIR,
81             '???' => 0xFF,
82             );
83              
84             =head2 new
85              
86             Create empty C object without loading disk image directory yet:
87              
88             my $d64DiskDirObj = D64::Disk::Dir->new();
89              
90             Create new C object and read entire D64/D71/D81 disk image directory from file on disk for further access.
91              
92             my $d64DiskDirObj = D64::Disk::Dir->new($filename);
93              
94             A valid C object is returned upon success, an undefined value otherwise.
95              
96             =cut
97              
98             sub new {
99 35     35 1 241032 my $this = shift;
100 35   33     201 my $class = ref($this) || $this;
101 35         70 my $self = {};
102 35         66 bless $self, $class;
103 35         125 my $initOK = $self->_initialize(@_);
104 35 50       68 if ($initOK) {
105 35         101 return $self;
106             }
107             else {
108 0         0 return undef;
109             }
110             }
111              
112             sub _initialize {
113 35     35   62 my $self = shift;
114 35         51 my $filename = shift;
115             # Read entire disk image directory:
116 35 100       87 if (defined $filename) {
117 33         95 my $readOK = $self->read_dir($filename);
118 33 50       80 return 0 unless $readOK;
119             }
120 35         61 return 1;
121             }
122              
123             sub _check_dir_read {
124 114     114   164 my $self = shift;
125             # Raise error if directory has not been read yet:
126 114 50       247 croak "Unable to perform requested operation, because disk image directory has not been read yet" if $self->{'DIR_READ'} == 0;
127             }
128              
129             sub _init_dir {
130 34     34   45 my $self = shift;
131             # Directory has not been read yet:
132 34         85 $self->{'DIR_READ'} = 0;
133 34         78 $self->_release_d64_image();
134 34         91 $self->_clear_dir_entries();
135 34         48 delete $self->{'D64_FILE_NAME'};
136 34         59 delete $self->{'DIR_INFO'};
137             }
138              
139             =head2 read_dir
140              
141             Read entire D64/D71/D81 disk image directory from file on disk, replacing currently loaded directory (if any).
142              
143             $d64DiskDirObj->read_dir($filename);
144              
145             Returns true value upon success, and false otherwise.
146              
147             =cut
148              
149             sub read_dir {
150 34     34 1 56 my $self = shift;
151 34         49 my $filename = shift;
152             # We do not verify file existence here, D64::Disk::Image module croaks on inexisting files:
153 34         116 $self->_init_dir();
154             # Load image into RAM:
155 34         90 my $d64DiskImageObj = D64::Disk::Image->load_image($filename);
156 34         4900 $self->{'D64_DISK_IMAGE'} = $d64DiskImageObj;
157 34         84 $self->{'D64_FILE_NAME'} = $filename;
158             # Open directory for reading:
159 34         119 my $dir = $d64DiskImageObj->open('$', T_PRG, F_READ);
160             # Get disk-wide directory information:
161 34         1584 $self->_get_dir_info($dir);
162             # Read directory blocks:
163 34         90 my $readOK = $self->_read_dir_blocks($dir);
164 34 50       77 return 0 unless $readOK;
165             # Close directory:
166 34         94 $dir->close();
167             # Store D64 disk image filename for further checks:
168 34         278 $self->{'FILENAME'} = $filename;
169             # Directory has been read successfully:
170 34         62 $self->{'DIR_READ'} = 1;
171 34         121 return 1;
172             # There was a problem reading directory:
173 0         0 return 0;
174             }
175              
176             sub _read_dir_blocks {
177 34     34   47 my $self = shift;
178 34         49 my $dir = shift;
179             # Read first block into buffer:
180 34         95 my ($counter, $buffer) = $dir->read(254);
181 34 50       531 if ($counter != 254) {
182 0         0 carp 'BAM read failed';
183 0         0 return 0;
184             }
185             # Read directory blocks:
186 34         57 while (1) {
187 68         147 my ($counter, $buffer) = $dir->read(254);
188 68 100       832 last unless $counter == 254;
189 34         86 for (my $offset = -2; $offset < 254; $offset += 32) {
190             # If file type != 0:
191 272         417 my $file_type = ord (substr $buffer, $offset + 2, 1);
192 272 100       566 if ($file_type != 0) {
193             # Create new D64::Disk::Dir::Entry object:
194 102         190 my $bytes = substr $buffer, $offset + 2, 30;
195 102         276 my $entryObj = D64::Disk::Dir::Entry->new($bytes);
196 102 50       197 unless (defined $entryObj) {
197 0         0 carp 'Directory blocks read failed';
198 0         0 return 0;
199             }
200             # Add it to the list of directory entries:
201 102         182 $self->_add_dir_entry($entryObj);
202             }
203             }
204             }
205 34         69 return 1;
206             }
207              
208             sub _get_dir_info {
209 34     34   49 my $self = shift;
210 34         49 my $dir = shift;
211 34         53 my $d64DiskImageObj = $self->{'D64_DISK_IMAGE'};
212             # Get number of blocks free:
213 34         80 my $blocksFree = $d64DiskImageObj->blocksfree();
214             # Get title and ID:
215 34         287 my ($title, $id) = $d64DiskImageObj->title();
216 34         537 $title = D64::Disk::Image->name_from_rawname($title);
217             # Store directory details in a hash:
218 34         411 $self->{'DIR_INFO'} = {
219             'TITLE' => $title,
220             'ID' => $id,
221             'BLOCKS_FREE' => $blocksFree,
222             };
223             }
224              
225             sub _add_dir_entry {
226 102     102   166 my $self = shift;
227 102         154 my $entryObj = shift;
228 102         138 push @{$self->{'DIR_ENTRIES'}}, $entryObj;
  102         326  
229             }
230              
231             sub _get_dir_entries {
232 90     90   133 my $self = shift;
233 90         123 my $entries = $self->{'DIR_ENTRIES'};
234 90 100       162 $entries = [] unless defined $entries;
235 90         133 return $entries;
236             }
237              
238             sub _clear_dir_entries {
239 34     34   50 my $self = shift;
240 34         75 $self->{'DIR_ENTRIES'} = [];
241             }
242              
243             =head2 get_title
244              
245             Get 16 character disk directory title (PETSCII string):
246              
247             my $convert2ascii = 0;
248             my $title = $d64DiskDirObj->get_title($convert2ascii);
249              
250             Get disk directory title converted to ASCII string:
251              
252             my $convert2ascii = 1;
253             my $title = $d64DiskDirObj->get_title($convert2ascii);
254              
255             =cut
256              
257             sub get_title {
258 5     5 1 16 my $self = shift;
259 5         8 my $convert2ascii = shift;
260 5         11 $self->_check_dir_read();
261 5         10 my $title = $self->{'DIR_INFO'}->{'TITLE'};
262             # Convert title to ASCII when necessary:
263 5 50       34 $title = D64::Disk::Image->petscii_to_ascii($title) if $convert2ascii;
264 5         771 return $title;
265             }
266              
267             =head2 get_id
268              
269             Get 5 character disk directory ID (PETSCII string):
270              
271             my $convert2ascii = 0;
272             my $diskID = $d64DiskDirObj->get_id($convert2ascii);
273              
274             Get disk directory ID converted to ASCII string:
275              
276             my $convert2ascii = 1;
277             my $diskID = $d64DiskDirObj->get_id($convert2ascii);
278              
279             =cut
280              
281             sub get_id {
282 5     5 1 28 my $self = shift;
283 5         6 my $convert2ascii = shift;
284 5         12 $self->_check_dir_read();
285 5         10 my $id = $self->{'DIR_INFO'}->{'ID'};
286             # Convert disk ID to ASCII when necessary:
287 5 50       21 $id = D64::Disk::Image->petscii_to_ascii($id) if $convert2ascii;
288 5         277 return $id;
289             }
290              
291             =head2 get_blocks_free
292              
293             Get number of blocks free:
294              
295             my $blocksFree = $d64DiskDirObj->get_blocks_free();
296              
297             =cut
298              
299             sub get_blocks_free {
300 5     5 1 14 my $self = shift;
301 5         12 $self->_check_dir_read();
302 5         13 my $blocksFree = $self->{'DIR_INFO'}->{'BLOCKS_FREE'};
303 5         9 return $blocksFree;
304             }
305              
306             =head2 num_entries
307              
308             Get number of directory entries:
309              
310             my $num_entries = $d64DiskDirObj->num_entries();
311              
312             =cut
313              
314             sub num_entries {
315 46     46 1 77 my $self = shift;
316 46         92 $self->_check_dir_read();
317 46         99 my $entries_aref = $self->_get_dir_entries();
318 46         65 my $num_entries = @{$entries_aref};
  46         73  
319 46         79 return $num_entries;
320             }
321              
322             =head2 get_entry
323              
324             Get directory entry at the specified position (index value must be a valid position equal or greater than 0 and less than number of directory entries):
325              
326             my $entryObj = $d64DiskDirObj->get_entry($index);
327              
328             Returns a valid L object upon success, and false otherwise.
329              
330             =cut
331              
332             sub get_entry {
333 41     41 1 220457 my $self = shift;
334 41         49 my $index = shift;
335 41         99 $self->_check_dir_read();
336 41         75 my $num_entries = $self->num_entries();
337 41 50 33     180 if ($index < 0 or $index >= $num_entries) {
338 0         0 carp "Cannot get entry at invalid index position (disk directory contains only ${num_entries} file(s), unable to get entry at position ${index})";
339 0         0 return undef;
340             }
341 41         75 my $entries_aref = $self->_get_dir_entries();
342 41         77 my $entryObj = $entries_aref->[$index];
343 41         69 return $entryObj;
344             }
345              
346             =head2 get_file_data
347              
348             Get binary file data from a directory entry at the specified position:
349              
350             my $data = $d64DiskDirObj->get_file_data($index);
351              
352             Reads data from a file at the specified directory index position (index value must be a valid position equal or greater than 0 and less than number of directory entries). Returns binary file data (including its loading address) upon success, and an undefined value otherwise.
353              
354             =cut
355              
356             sub get_file_data {
357 8     8 1 1495 my $self = shift;
358 8         9 my $index = shift;
359 8         19 $self->_check_dir_read();
360 8         16 my $entryObj = $self->get_entry($index);
361 8 50       17 unless (defined $entryObj) {
362 0         0 carp "Unable to get file data from an inexisting directory entry (validate first that ${index} file(s) really exist(s) on this disk!)";
363 0         0 return undef;
364             }
365 8         12 my $d64DiskImageObj = $self->{'D64_DISK_IMAGE'};
366              
367             # Validate initial track/sector:
368 8         20 my $track = $entryObj->get_track();
369 8         18 my $sector = $entryObj->get_sector();
370 8         24 my $imageType = $d64DiskImageObj->type();
371 8         82 my $tracks = $d64DiskImageObj->tracks($imageType);
372 8 100 66     72 if ($track < 1 || $track > $tracks) {
373 1         281 carp "Unable to get file data from an illegal track (validate first that initial track ${track} really exists on this disk!)";
374 1         202 return undef;
375             }
376 7         19 my $sectors_per_track = $d64DiskImageObj->sectors_per_track($imageType, $track);
377 7 100 66     93 if ($sector < 0 || $sector >= $sectors_per_track) {
378 1         129 carp "Unable to get file data from an illegal sector (validate first that initial sector ${sector} really exists on track ${track}!)";
379 1         168 return undef;
380             }
381              
382             # Get filename from the specified directory index position:
383 6         15 my $name = $entryObj->get_name(0);
384 6         21 my $rawname = D64::Disk::Image->rawname_from_name($name);
385             # Get the actual filetype:
386 6         71 my $type = $entryObj->get_type();
387 6         12 my $filetype = $file_type_constants{$type};
388             # Open a file for reading:
389 6         26 my $prg = $d64DiskImageObj->open($rawname, $filetype, F_READ);
390             # Read data from file:
391 6         281 my ($counter, $buffer) = $prg->read();
392             # Close a file:
393 6         2764 $prg->close();
394 6         63 return $buffer;
395             }
396              
397             =head2 print_dir
398              
399             Print out the entire directory content to any opened file handle (the standard output by default):
400              
401             $d64DiskDirObj->print_dir($fh, { verbose => $verbose });
402              
403             C defaults to false (changing it to true will additionally print out all files' track, sector, and loading address values).
404              
405             =cut
406              
407             sub print_dir {
408 4     4 1 3070 my ($self, $fh, $args) = @_;
409 4 100       19 $fh = *STDOUT unless defined $fh;
410 4 100       16 $args = {} unless defined $args;
411 4         11 my $verbose = $args->{verbose};
412 4         16 $self->_check_dir_read();
413 4         33 $self->_print_title($fh);
414 4         30 my $num_entries = $self->num_entries();
415 4         15 for (my $i = 0; $i < $num_entries; $i++) {
416 12         162 my $entryObj = $self->get_entry($i);
417             # Set up the loading address:
418 12         23 my $loading_address = '';
419 12 100       37 if ($verbose) {
420             # Get the actual file type:
421 6         19 my $type = $entryObj->get_type();
422 6         16 my $filetype = $file_type_constants{$type};
423             # Read the file data only for PRG files:
424 6 50       17 if ($filetype == T_PRG) {
425             # Do not attempt to read loading address of a non-closed file:
426 6 100       14 if ($entryObj->get_closed()) {
427             # Compute the loading address:
428 5         13 my $data = $self->get_file_data($i);
429 5 50       25 if ($data) {
430 5         20 my ($lo, $hi) = map { ord } split //, substr $data, 0, 2;
  10         33  
431 5         34 $loading_address = sprintf ' $%04x', $lo + $hi * 256;
432             }
433             }
434             }
435             }
436 12         44 my $print_fh = new IO::Scalar;
437 12         433 $entryObj->print_entry($print_fh, { verbose => $verbose });
438 12         205 my $entry_content = ${$print_fh->sref};
  12         32  
439             # Append the loading address to printed entry:
440 12         140 chomp $entry_content;
441 12         80 $entry_content .= $loading_address . "\n";
442             # Print entry to $fh:
443 12         196 print $fh $entry_content;
444             }
445 4         94 $self->_print_blocks_free($fh);
446             }
447              
448             sub _print_title {
449 4     4   9 my $self = shift;
450 4         9 my $fh = shift;
451             # Get title converted to ASCII:
452 4         10 my $title = $self->get_title(1);
453             # Get disk ID converted to ASCII:
454 4         22 my $id = $self->get_id(1);
455             # Print title and disk ID:
456 4         124 print $fh '0 ';
457 4         55 my $header_text = sprintf "\"%-16s\" %-5s", $title, $id;
458 4         98 print $fh REVERSE, $header_text, RESET;
459 4         769 print $fh "\n";
460             }
461              
462             sub _print_blocks_free {
463 4     4   10 my $self = shift;
464 4         9 my $fh = shift;
465             # Print number of blocks free:
466 4         8 my $blocksFree = $self->get_blocks_free();
467 4         72 printf $fh "%d blocks free\n", $blocksFree;
468             }
469              
470             sub DESTROY {
471 35     35   14367 my $self = shift;
472 35         75 $self->_release_d64_image();
473             }
474              
475             sub _release_d64_image {
476 69     69   116 my $self = shift;
477 69         111 my $d64DiskImageObj = $self->{'D64_DISK_IMAGE'};
478 69         114 delete $self->{'D64_DISK_IMAGE'};
479             # Release D64 image:
480 69 100       386 $d64DiskImageObj->free_image() if defined $d64DiskImageObj;
481             }
482              
483             =head1 BUGS
484              
485             There are no known bugs at the moment. Please report any bugs or feature requests.
486              
487             =head1 EXPORT
488              
489             None. No method is exported into the caller's namespace either by default or explicitly.
490              
491             =head1 SEE ALSO
492              
493             L, L, L
494              
495             =head1 AUTHOR
496              
497             Pawel Krol, Epawelkrol@cpan.orgE.
498              
499             =head1 VERSION
500              
501             Version 0.05 (2023-05-14)
502              
503             =head1 COPYRIGHT AND LICENSE
504              
505             This module is licensed under a slightly modified BSD license, the same terms as Per Olofsson's "diskimage.c" library and L Perl package it is based on, license contents are repeated below.
506              
507             Copyright (c) 2003-2006, Per Olofsson
508             All rights reserved.
509              
510             Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
511              
512             =over
513              
514             =item *
515              
516             Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
517              
518             =item *
519              
520             Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
521              
522             =back
523              
524             THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
525              
526             diskimage.c website: L
527              
528             =cut
529              
530             1;