File Coverage

blib/lib/D64/Disk/Dir.pm
Criterion Covered Total %
statement 158 168 94.0
branch 22 32 68.7
condition 2 6 33.3
subroutine 28 28 100.0
pod 9 9 100.0
total 219 243 90.1


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   278610 use bytes;
  4         62  
  4         16  
53 4     4   100 use strict;
  4         5  
  4         59  
54 4     4   16 use warnings;
  4         6  
  4         102  
55              
56 4     4   18 use base qw( Exporter );
  4         7  
  4         658  
57             our %EXPORT_TAGS = ();
58             $EXPORT_TAGS{'all'} = [];
59             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
60             our @EXPORT = qw();
61              
62             our $VERSION = '0.04';
63              
64 4     4   23 use Carp qw/carp croak verbose/;
  4         8  
  4         500  
65              
66 4     4   1500 use D64::Disk::Dir::Entry;
  4         8  
  4         111  
67 4     4   20 use D64::Disk::Image qw(:all);
  4         7  
  4         5312  
68              
69             # Mapping file types onto file type constants:
70             our %file_type_constants = (
71             'del' => T_DEL,
72             'seq' => T_SEQ,
73             'prg' => T_PRG,
74             'usr' => T_USR,
75             'rel' => T_REL,
76             'cbm' => T_CBM,
77             'dir' => T_DIR,
78             '???' => 0xFF,
79             );
80              
81             =head2 new
82              
83             Create empty C object without loading disk image directory yet:
84              
85             my $d64DiskDirObj = D64::Disk::Dir->new();
86              
87             Create new C object and read entire D64/D71/D81 disk image directory from file on disk for further access.
88              
89             my $d64DiskDirObj = D64::Disk::Dir->new($filename);
90              
91             A valid C object is returned upon success, an undefined value otherwise.
92              
93             =cut
94              
95             sub new {
96 26     26 1 25992 my $this = shift;
97 26   33     106 my $class = ref($this) || $this;
98 26         39 my $self = {};
99 26         43 bless $self, $class;
100 26         55 my $initOK = $self->_initialize(@_);
101 26 50       40 if ($initOK) {
102 26         61 return $self;
103             }
104             else {
105 0         0 return undef;
106             }
107             }
108              
109             sub _initialize {
110 26     26   31 my $self = shift;
111 26         33 my $filename = shift;
112             # Read entire disk image directory:
113 26 100       49 if (defined $filename) {
114 24         46 my $readOK = $self->read_dir($filename);
115 24 50       58 return 0 unless $readOK;
116             }
117 26         35 return 1;
118             }
119              
120             sub _check_dir_read {
121 55     55   57 my $self = shift;
122             # Raise error if directory has not been read yet:
123 55 50       95 croak "Unable to perform requested operation, because disk image directory has not been read yet" if $self->{'DIR_READ'} == 0;
124             }
125              
126             sub _init_dir {
127 25     25   32 my $self = shift;
128             # Directory has not been read yet:
129 25         47 $self->{'DIR_READ'} = 0;
130 25         46 $self->_release_d64_image();
131 25         52 $self->_clear_dir_entries();
132 25         29 delete $self->{'D64_FILE_NAME'};
133 25         30 delete $self->{'DIR_INFO'};
134             }
135              
136             =head2 read_dir
137              
138             Read entire D64/D71/D81 disk image directory from file on disk, replacing currently loaded directory (if any).
139              
140             $d64DiskDirObj->read_dir($filename);
141              
142             Returns true value upon success, and false otherwise.
143              
144             =cut
145              
146             sub read_dir {
147 25     25 1 45 my $self = shift;
148 25         30 my $filename = shift;
149             # We do not verify file existence here, D64::Disk::Image module croaks on inexisting files:
150 25         51 $self->_init_dir();
151             # Load image into RAM:
152 25         65 my $d64DiskImageObj = D64::Disk::Image->load_image($filename);
153 25         2911 $self->{'D64_DISK_IMAGE'} = $d64DiskImageObj;
154 25         42 $self->{'D64_FILE_NAME'} = $filename;
155             # Open directory for reading:
156 25         66 my $dir = $d64DiskImageObj->open('$', T_PRG, F_READ);
157             # Get disk-wide directory information:
158 25         901 $self->_get_dir_info($dir);
159             # Read directory blocks:
160 25         44 my $readOK = $self->_read_dir_blocks($dir);
161 25 50       41 return 0 unless $readOK;
162             # Close directory:
163 25         53 $dir->close();
164             # Store D64 disk image filename for further checks:
165 25         154 $self->{'FILENAME'} = $filename;
166             # Directory has been read successfully:
167 25         32 $self->{'DIR_READ'} = 1;
168 25         71 return 1;
169             # There was a problem reading directory:
170 0         0 return 0;
171             }
172              
173             sub _read_dir_blocks {
174 25     25   32 my $self = shift;
175 25         27 my $dir = shift;
176             # Read first block into buffer:
177 25         51 my ($counter, $buffer) = $dir->read(254);
178 25 50       276 if ($counter != 254) {
179 0         0 carp 'BAM read failed';
180 0         0 return 0;
181             }
182             # Read directory blocks:
183 25         32 while (1) {
184 50         85 my ($counter, $buffer) = $dir->read(254);
185 50 100       484 last unless $counter == 254;
186 25         66 for (my $offset = -2; $offset < 254; $offset += 32) {
187             # If file type != 0:
188 200         265 my $file_type = ord (substr $buffer, $offset + 2, 1);
189 200 100       329 if ($file_type != 0) {
190             # Create new D64::Disk::Dir::Entry object:
191 75         103 my $bytes = substr $buffer, $offset + 2, 30;
192 75         165 my $entryObj = D64::Disk::Dir::Entry->new($bytes);
193 75 50       115 unless (defined $entryObj) {
194 0         0 carp 'Directory blocks read failed';
195 0         0 return 0;
196             }
197             # Add it to the list of directory entries:
198 75         408 $self->_add_dir_entry($entryObj);
199             }
200             }
201             }
202 25         33 return 1;
203             }
204              
205             sub _get_dir_info {
206 25     25   31 my $self = shift;
207 25         27 my $dir = shift;
208 25         29 my $d64DiskImageObj = $self->{'D64_DISK_IMAGE'};
209             # Get number of blocks free:
210 25         48 my $blocksFree = $d64DiskImageObj->blocksfree();
211             # Get title and ID:
212 25         161 my ($title, $id) = $d64DiskImageObj->title();
213 25         287 $title = D64::Disk::Image->name_from_rawname($title);
214 25         198 $id = D64::Disk::Image->name_from_rawname($id);
215             # Store directory details in a hash:
216 25         253 $self->{'DIR_INFO'} = {
217             'TITLE' => $title,
218             'ID' => $id,
219             'BLOCKS_FREE' => $blocksFree,
220             };
221             }
222              
223             sub _add_dir_entry {
224 75     75   82 my $self = shift;
225 75         75 my $entryObj = shift;
226 75         82 push @{$self->{'DIR_ENTRIES'}}, $entryObj;
  75         186  
227             }
228              
229             sub _get_dir_entries {
230 46     46   48 my $self = shift;
231 46         51 my $entries = $self->{'DIR_ENTRIES'};
232 46 100       65 $entries = [] unless defined $entries;
233 46         59 return $entries;
234             }
235              
236             sub _clear_dir_entries {
237 25     25   32 my $self = shift;
238 25         45 $self->{'DIR_ENTRIES'} = [];
239             }
240              
241             =head2 get_title
242              
243             Get 16 character disk directory title (PETSCII string):
244              
245             my $convert2ascii = 0;
246             my $title = $d64DiskDirObj->get_title($convert2ascii);
247              
248             Get disk directory title converted to ASCII string:
249              
250             my $convert2ascii = 1;
251             my $title = $d64DiskDirObj->get_title($convert2ascii);
252              
253             =cut
254              
255             sub get_title {
256 3     3 1 8 my $self = shift;
257 3         4 my $convert2ascii = shift;
258 3         4 $self->_check_dir_read();
259 3         4 my $title = $self->{'DIR_INFO'}->{'TITLE'};
260             # Convert title to ASCII when necessary:
261 3 50       19 $title = D64::Disk::Image->petscii_to_ascii($title) if $convert2ascii;
262 3         355 return $title;
263             }
264              
265             =head2 get_id
266              
267             Get 2 character disk directory ID (PETSCII string):
268              
269             my $convert2ascii = 0;
270             my $diskID = $d64DiskDirObj->get_id($convert2ascii);
271              
272             Get disk directory ID converted to ASCII string:
273              
274             my $convert2ascii = 1;
275             my $diskID = $d64DiskDirObj->get_id($convert2ascii);
276              
277             =cut
278              
279             sub get_id {
280 3     3 1 9 my $self = shift;
281 3         3 my $convert2ascii = shift;
282 3         16 $self->_check_dir_read();
283 3         5 my $id = $self->{'DIR_INFO'}->{'ID'};
284             # Convert disk ID to ASCII when necessary:
285 3 50       16 $id = D64::Disk::Image->petscii_to_ascii($id) if $convert2ascii;
286 3         67 return $id;
287             }
288              
289             =head2 get_blocks_free
290              
291             Get number of blocks free:
292              
293             my $blocksFree = $d64DiskDirObj->get_blocks_free();
294              
295             =cut
296              
297             sub get_blocks_free {
298 3     3 1 10 my $self = shift;
299 3         6 $self->_check_dir_read();
300 3         4 my $blocksFree = $self->{'DIR_INFO'}->{'BLOCKS_FREE'};
301 3         4 return $blocksFree;
302             }
303              
304             =head2 num_entries
305              
306             Get number of directory entries:
307              
308             my $num_entries = $d64DiskDirObj->num_entries();
309              
310             =cut
311              
312             sub num_entries {
313 23     23 1 32 my $self = shift;
314 23         36 $self->_check_dir_read();
315 23         37 my $entries_aref = $self->_get_dir_entries();
316 23         26 my $num_entries = @{$entries_aref};
  23         31  
317 23         30 return $num_entries;
318             }
319              
320             =head2 get_entry
321              
322             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):
323              
324             my $entryObj = $d64DiskDirObj->get_entry($index);
325              
326             Returns a valid L object upon success, and false otherwise.
327              
328             =cut
329              
330             sub get_entry {
331 20     20 1 47 my $self = shift;
332 20         21 my $index = shift;
333 20         40 $self->_check_dir_read();
334 20         40 my $num_entries = $self->num_entries();
335 20 50 33     72 if ($index < 0 or $index >= $num_entries) {
336 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})";
337 0         0 return undef;
338             }
339 20         30 my $entries_aref = $self->_get_dir_entries();
340 20         24 my $entryObj = $entries_aref->[$index];
341 20         25 return $entryObj;
342             }
343              
344             =head2 get_file_data
345              
346             Get binary file data from a directory entry at the specified position:
347              
348             my $data = $d64DiskDirObj->get_file_data($index);
349              
350             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.
351              
352             =cut
353              
354             sub get_file_data {
355 1     1 1 7 my $self = shift;
356 1         2 my $index = shift;
357 1         3 $self->_check_dir_read();
358 1         2 my $entryObj = $self->get_entry($index);
359 1 50       3 unless (defined $entryObj) {
360 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!)";
361 0         0 return undef;
362             }
363 1         2 my $d64DiskImageObj = $self->{'D64_DISK_IMAGE'};
364             # Get filename from the specified directory index position:
365 1         4 my $name = $entryObj->get_name(0);
366 1         3 my $rawname = D64::Disk::Image->rawname_from_name($name);
367             # Get the actual filetype:
368 1         10 my $type = $entryObj->get_type();
369 1         3 my $filetype = $file_type_constants{$type};
370             # Open a file for reading:
371 1         3 my $prg = $d64DiskImageObj->open($rawname, $filetype, F_READ);
372             # Read data from file:
373 1         34 my ($counter, $buffer) = $prg->read();
374             # Close a file:
375 1         746 $prg->close();
376 1         10 return $buffer;
377             }
378              
379             =head2 print_dir
380              
381             Print out the entire directory content to any opened file handle (the standard output by default):
382              
383             $d64DiskDirObj->print_dir($fh);
384              
385             =cut
386              
387             sub print_dir {
388 2     2 1 811 my $self = shift;
389 2         3 my $fh = shift;
390 2 100       7 $fh = *STDOUT unless defined $fh;
391 2         5 $self->_check_dir_read();
392 2         5 $self->_print_title($fh);
393 2         24 my $num_entries = $self->num_entries();
394 2         5 for (my $i = 0; $i < $num_entries; $i++) {
395 6         36 my $entryObj = $self->get_entry($i);
396 6         14 $entryObj->print_entry($fh);
397             }
398 2         17 $self->_print_blocks_free($fh);
399             }
400              
401             sub _print_title {
402 2     2   3 my $self = shift;
403 2         2 my $fh = shift;
404             # Get title converted to ASCII:
405 2         5 my $title = $self->get_title(1);
406             # Get disk ID converted to ASCII:
407 2         5 my $id = $self->get_id(1);
408             # Print title and disk ID:
409 2         30 printf $fh "0 \"%-16s\" %s\n", $title, $id;
410             }
411              
412             sub _print_blocks_free {
413 2     2   4 my $self = shift;
414 2         3 my $fh = shift;
415             # Print number of blocks free:
416 2         5 my $blocksFree = $self->get_blocks_free();
417 2         18 printf $fh "%d blocks free\n", $blocksFree;
418             }
419              
420             sub DESTROY {
421 26     26   7668 my $self = shift;
422 26         49 $self->_release_d64_image();
423             }
424              
425             sub _release_d64_image {
426 51     51   56 my $self = shift;
427 51         67 my $d64DiskImageObj = $self->{'D64_DISK_IMAGE'};
428 51         66 delete $self->{'D64_DISK_IMAGE'};
429             # Release D64 image:
430 51 100       249 $d64DiskImageObj->free_image() if defined $d64DiskImageObj;
431             }
432              
433             =head1 BUGS
434              
435             There are no known bugs at the moment. Please report any bugs or feature requests.
436              
437             =head1 EXPORT
438              
439             None. No method is exported into the caller's namespace either by default or explicitly.
440              
441             =head1 SEE ALSO
442              
443             L, L, L
444              
445             =head1 AUTHOR
446              
447             Pawel Krol, Epawelkrol@cpan.orgE.
448              
449             =head1 VERSION
450              
451             Version 0.04 (2018-11-25)
452              
453             =head1 COPYRIGHT AND LICENSE
454              
455             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.
456              
457             Copyright (c) 2003-2006, Per Olofsson
458             All rights reserved.
459              
460             Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
461              
462             =over
463              
464             =item *
465              
466             Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
467              
468             =item *
469              
470             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.
471              
472             =back
473              
474             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.
475              
476             diskimage.c website: L
477              
478             =cut
479              
480             1;