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   425234 use bytes;
  4         82  
  4         28  
53 4     4   149 use strict;
  4         8  
  4         98  
54 4     4   21 use warnings;
  4         15  
  4         125  
55              
56 4     4   32 use base qw( Exporter );
  4         6  
  4         906  
57             our %EXPORT_TAGS = ();
58             $EXPORT_TAGS{'all'} = [];
59             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
60             our @EXPORT = qw();
61              
62             our $VERSION = '0.06';
63              
64 4     4   30 use Carp qw/carp croak verbose/;
  4         6  
  4         669  
65 4     4   2018 use Data::Dumper;
  4         20439  
  4         242  
66 4     4   1001 use IO::Scalar;
  4         25993  
  4         223  
67 4     4   2817 use Term::ANSIColor qw(:constants);
  4         38726  
  4         5451  
68              
69 4     4   2154 use D64::Disk::Dir::Entry;
  4         10  
  4         135  
70 4     4   26 use D64::Disk::Image qw(:all);
  4         8  
  4         8403  
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 235667 my $this = shift;
100 35   33     213 my $class = ref($this) || $this;
101 35         111 my $self = {};
102 35         68 bless $self, $class;
103 35         91 my $initOK = $self->_initialize(@_);
104 35 50       68 if ($initOK) {
105 35         92 return $self;
106             }
107             else {
108 0         0 return undef;
109             }
110             }
111              
112             sub _initialize {
113 35     35   53 my $self = shift;
114 35         57 my $filename = shift;
115             # Read entire disk image directory:
116 35 100       84 if (defined $filename) {
117 33         82 my $readOK = $self->read_dir($filename);
118 33 50       85 return 0 unless $readOK;
119             }
120 35         60 return 1;
121             }
122              
123             sub _check_dir_read {
124 114     114   154 my $self = shift;
125             # Raise error if directory has not been read yet:
126 114 50       232 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   48 my $self = shift;
131             # Directory has not been read yet:
132 34         75 $self->{'DIR_READ'} = 0;
133 34         84 $self->_release_d64_image();
134 34         90 $self->_clear_dir_entries();
135 34         50 delete $self->{'D64_FILE_NAME'};
136 34         50 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         78 $self->_init_dir();
154             # Load image into RAM:
155 34         93 my $d64DiskImageObj = D64::Disk::Image->load_image($filename);
156 34         4890 $self->{'D64_DISK_IMAGE'} = $d64DiskImageObj;
157 34         79 $self->{'D64_FILE_NAME'} = $filename;
158             # Open directory for reading:
159 34         109 my $dir = $d64DiskImageObj->open('$', T_PRG, F_READ);
160             # Get disk-wide directory information:
161 34         1611 $self->_get_dir_info($dir);
162             # Read directory blocks:
163 34         79 my $readOK = $self->_read_dir_blocks($dir);
164 34 50       76 return 0 unless $readOK;
165             # Close directory:
166 34         94 $dir->close();
167             # Store D64 disk image filename for further checks:
168 34         257 $self->{'FILENAME'} = $filename;
169             # Directory has been read successfully:
170 34         47 $self->{'DIR_READ'} = 1;
171 34         115 return 1;
172             # There was a problem reading directory:
173 0         0 return 0;
174             }
175              
176             sub _read_dir_blocks {
177 34     34   50 my $self = shift;
178 34         47 my $dir = shift;
179             # Read first block into buffer:
180 34         81 my ($counter, $buffer) = $dir->read(254);
181 34 50       471 if ($counter != 254) {
182 0         0 carp 'BAM read failed';
183 0         0 return 0;
184             }
185             # Read directory blocks:
186 34         56 while (1) {
187 68         141 my ($counter, $buffer) = $dir->read(254);
188 68 100       816 last unless $counter == 254;
189 34         89 for (my $offset = -2; $offset < 254; $offset += 32) {
190             # If file type != 0:
191 272         428 my $file_type = ord (substr $buffer, $offset + 2, 1);
192 272 100       619 if ($file_type != 0) {
193             # Create new D64::Disk::Dir::Entry object:
194 102         179 my $bytes = substr $buffer, $offset + 2, 30;
195 102         271 my $entryObj = D64::Disk::Dir::Entry->new($bytes);
196 102 50       185 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         185 $self->_add_dir_entry($entryObj);
202             }
203             }
204             }
205 34         65 return 1;
206             }
207              
208             sub _get_dir_info {
209 34     34   61 my $self = shift;
210 34         47 my $dir = shift;
211 34         55 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         481 $title = D64::Disk::Image->name_from_rawname($title);
217             # Store directory details in a hash:
218 34         393 $self->{'DIR_INFO'} = {
219             'TITLE' => $title,
220             'ID' => $id,
221             'BLOCKS_FREE' => $blocksFree,
222             };
223             }
224              
225             sub _add_dir_entry {
226 102     102   150 my $self = shift;
227 102         139 my $entryObj = shift;
228 102         137 push @{$self->{'DIR_ENTRIES'}}, $entryObj;
  102         307  
229             }
230              
231             sub _get_dir_entries {
232 90     90   156 my $self = shift;
233 90         123 my $entries = $self->{'DIR_ENTRIES'};
234 90 100       503 $entries = [] unless defined $entries;
235 90         141 return $entries;
236             }
237              
238             sub _clear_dir_entries {
239 34     34   50 my $self = shift;
240 34         70 $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         7 my $convert2ascii = shift;
260 5         31 $self->_check_dir_read();
261 5         9 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         738 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 25 my $self = shift;
283 5         7 my $convert2ascii = shift;
284 5         12 $self->_check_dir_read();
285 5         21 my $id = $self->{'DIR_INFO'}->{'ID'};
286             # Convert disk ID to ASCII when necessary:
287 5 50       24 $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 19 my $self = shift;
301 5         15 $self->_check_dir_read();
302 5         11 my $blocksFree = $self->{'DIR_INFO'}->{'BLOCKS_FREE'};
303 5         7 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 83 my $self = shift;
316 46         96 $self->_check_dir_read();
317 46         112 my $entries_aref = $self->_get_dir_entries();
318 46         58 my $num_entries = @{$entries_aref};
  46         62  
319 46         85 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 218808 my $self = shift;
334 41         57 my $index = shift;
335 41         89 $self->_check_dir_read();
336 41         80 my $num_entries = $self->num_entries();
337 41 50 33     166 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         105 my $entries_aref = $self->_get_dir_entries();
342 41         71 my $entryObj = $entries_aref->[$index];
343 41         101 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 1462 my $self = shift;
358 8         14 my $index = shift;
359 8         17 $self->_check_dir_read();
360 8         18 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         23 my $d64DiskImageObj = $self->{'D64_DISK_IMAGE'};
366              
367             # Validate initial track/sector:
368 8         23 my $track = $entryObj->get_track();
369 8         19 my $sector = $entryObj->get_sector();
370 8         29 my $imageType = $d64DiskImageObj->type();
371 8         81 my $tracks = $d64DiskImageObj->tracks($imageType);
372 8 100 66     82 if ($track < 1 || $track > $tracks) {
373 1         296 carp "Unable to get file data from an illegal track (validate first that initial track ${track} really exists on this disk!)";
374 1         206 return undef;
375             }
376 7         20 my $sectors_per_track = $d64DiskImageObj->sectors_per_track($imageType, $track);
377 7 100 66     74 if ($sector < 0 || $sector >= $sectors_per_track) {
378 1         140 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         29 my $rawname = D64::Disk::Image->rawname_from_name($name);
385             # Get the actual filetype:
386 6         69 my $type = $entryObj->get_type();
387 6         18 my $filetype = $file_type_constants{$type};
388             # Open a file for reading:
389 6         39 my $prg = $d64DiskImageObj->open($rawname, $filetype, F_READ);
390             # Read data from file:
391 6         292 my ($counter, $buffer) = $prg->read();
392             # Close a file:
393 6         2669 $prg->close();
394 6         77 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 2916 my ($self, $fh, $args) = @_;
409 4 100       17 $fh = *STDOUT unless defined $fh;
410 4 100       12 $args = {} unless defined $args;
411 4         11 my $verbose = $args->{verbose};
412 4         16 $self->_check_dir_read();
413 4         34 $self->_print_title($fh);
414 4         33 my $num_entries = $self->num_entries();
415 4         20 for (my $i = 0; $i < $num_entries; $i++) {
416 12         200 my $entryObj = $self->get_entry($i);
417             # Set up the loading address:
418 12         21 my $loading_address = '';
419 12 100       33 if ($verbose) {
420             # Get the actual file type:
421 6         24 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       14 if ($filetype == T_PRG) {
425             # Do not attempt to read loading address of a non-closed file:
426 6 100       23 if ($entryObj->get_closed()) {
427             # Compute the loading address:
428 5         25 my $data = $self->get_file_data($i);
429 5 50       18 if ($data) {
430 5         41 my ($lo, $hi) = map { ord } split //, substr $data, 0, 2;
  10         23  
431 5         29 $loading_address = sprintf ' $%04x', $lo + $hi * 256;
432             }
433             }
434             }
435             }
436 12         44 my $print_fh = new IO::Scalar;
437 12         440 $entryObj->print_entry($print_fh, { verbose => $verbose });
438 12         226 my $entry_content = ${$print_fh->sref};
  12         32  
439             # Append the loading address to printed entry:
440 12         69 chomp $entry_content;
441 12         22 $entry_content .= $loading_address . "\n";
442             # Print entry to $fh:
443 12         193 print $fh $entry_content;
444             }
445 4         80 $self->_print_blocks_free($fh);
446             }
447              
448             sub _print_title {
449 4     4   11 my $self = shift;
450 4         8 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         13 my $id = $self->get_id(1);
455             # Print title and disk ID:
456 4         122 print $fh '0 ';
457 4         58 my $header_text = sprintf "\"%-16s\" %-5s", $title, $id;
458 4         105 print $fh REVERSE, $header_text, RESET;
459 4         837 print $fh "\n";
460             }
461              
462             sub _print_blocks_free {
463 4     4   8 my $self = shift;
464 4         8 my $fh = shift;
465             # Print number of blocks free:
466 4         10 my $blocksFree = $self->get_blocks_free();
467 4         69 printf $fh "%d blocks free\n", $blocksFree;
468             }
469              
470             sub DESTROY {
471 35     35   14937 my $self = shift;
472 35         79 $self->_release_d64_image();
473             }
474              
475             sub _release_d64_image {
476 69     69   105 my $self = shift;
477 69         105 my $d64DiskImageObj = $self->{'D64_DISK_IMAGE'};
478 69         121 delete $self->{'D64_DISK_IMAGE'};
479             # Release D64 image:
480 69 100       340 $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.06 (2023-08-28)
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;