File Coverage

blib/lib/D64/Disk/Dir/Item.pm
Criterion Covered Total %
statement 305 317 96.2
branch 152 170 89.4
condition 33 45 73.3
subroutine 42 43 97.6
pod 19 24 79.1
total 551 599 91.9


line stmt bran cond sub pod time code
1             package D64::Disk::Dir::Item;
2              
3             =head1 NAME
4              
5             D64::Disk::Dir::Item - Handling individual Commodore (D64/D71/D81) disk image directory items in pure Perl
6              
7             =head1 SYNOPSIS
8              
9             use D64::Disk::Dir::Item qw(:all);
10              
11             # Create a new disk image directory item instance:
12             my $item = D64::Disk::Dir::Item->new($data);
13             my $item = D64::Disk::Dir::Item->new(@data);
14             my $item = D64::Disk::Dir::Item->new(\@data);
15              
16             # Fetch item data as a scalar of 30 bytes:
17             my $data = $item->data();
18             # Fetch item data as an array of 30 bytes:
19             my @data = $item->data();
20              
21             # Update item providing 30 bytes of scalar data:
22             $item->data($data);
23             # Update item given array with 30 bytes of data:
24             $item->data(@data);
25             $item->data(\@data);
26              
27             # Get/set the actual file type:
28             my $type = $item->type();
29             $item->type($type);
30              
31             # Get/set "closed" flag (when not set produces "*", or "splat" files):
32             my $is_closed = $item->closed();
33             $item->closed($is_closed);
34              
35             # Get/set "locked" flag (when set produces ">" locked files):
36             my $is_locked = $item->locked();
37             $item->locked($is_locked);
38              
39             # Get/set track location of first sector of file:
40             my $track = $item->track();
41             $item->track($track);
42              
43             # Get/set sector location of first sector of file:
44             my $sector = $item->sector();
45             $item->sector($sector);
46              
47             # Get/set 16 character filename (in CBM ASCII, padded with $A0):
48             my $name = $item->name();
49             $item->name($name);
50              
51             # Get/set track location of first side-sector block (REL file only):
52             my $side_track = $item->side_track();
53             $item->side_track($side_track);
54              
55             # Get/set sector location of first side-sector block (REL file only):
56             my $side_sector = $item->side_sector();
57             $item->side_sector($side_sector);
58              
59             # Get/set relative file record length (REL file only):
60             my $record_length = $item->record_length();
61             $item->record_length($record_length);
62              
63             # Get/set file size in sectors:
64             my $size = $item->size();
65             $item->size($size);
66              
67             # Print out formatted disk image directory item:
68             $item->print();
69              
70             # Validate item data against all possible errors:
71             my $is_valid = $item->validate();
72              
73             # Check if directory item contains information about the actual disk file:
74             my $is_empty = $item->empty();
75              
76             # Check if directory item is writable and can be replaced by any new file:
77             my $is_writable = $item->writable();
78              
79             # Clone disk directory item:
80             my $clone = $item->clone();
81              
82             # Check if filename matches given CBM ASCII pattern:
83             my $is_matched = $item->match_name($petscii_pattern);
84              
85             # Convert any given file type into its three-letter printable string representation:
86             my $string = D64::Disk::Dir::Item->type_to_string($type);
87              
88             =head1 DESCRIPTION
89              
90             C provides a helper class for C module, enabling users to manipulate individual directory entries in an object oriented way without the hassle of worrying about the meaning of individual bits and bytes describing each entry in a disk directory. The whole family of C modules has been implemented in pure Perl as an alternative to Per Olofsson's "diskimage.c" library originally written in an ANSI C.
91              
92             =head1 METHODS
93              
94             =cut
95              
96 8     8   350899 use bytes;
  8         78  
  8         43  
97 8     8   221 use strict;
  8         16  
  8         257  
98 8     8   6661 use utf8;
  8         77  
  8         44  
99 8     8   237 use warnings;
  8         12  
  8         415  
100              
101             our $VERSION = '0.07';
102              
103 8     8   6317 use parent 'Clone';
  8         2677  
  8         42  
104              
105 8     8   44620 use Data::Dumper;
  8         61382  
  8         615  
106 8     8   12900 use Readonly;
  8         24949  
  8         591  
107 8     8   69 use Scalar::Util qw(looks_like_number);
  8         17  
  8         1026  
108 8     8   7821 use Text::Convert::PETSCII qw(:convert);
  8         19244  
  8         1638  
109 8     8   7818 use Try::Tiny;
  8         15177  
  8         3279  
110              
111             require XSLoader;
112             XSLoader::load(__PACKAGE__, $VERSION);
113              
114             # Setup package constant names:
115             our (
116             # File type constant names:
117             $T_DEL, $T_SEQ, $T_PRG, $T_USR, $T_REL, $T_CBM, $T_DIR,
118              
119             # Data offset constant names:
120             $I_TYPE, $I_NAME, $I_CLOSED, $I_LOCKED,
121             $I_TRACK, $I_SECTOR, $I_SIZE_LO, $I_SIZE_HI,
122             $I_SIDE_TRACK, $I_SIDE_SECTOR, $I_RECORD_LENGTH,
123              
124             # Other package constant names:
125             $ITEM_SIZE,
126             );
127              
128             # File type constant values:
129             my %file_type_constants = (
130             T_DEL => 0b000,
131             T_SEQ => 0b001,
132             T_PRG => 0b010,
133             T_USR => 0b011,
134             T_REL => 0b100,
135             T_CBM => 0b101,
136             T_DIR => 0b110,
137             );
138              
139             # Data offset constant values:
140             my %data_offset_constants = (
141             I_TYPE => 0x00,
142             I_CLOSED => 0x00,
143             I_LOCKED => 0x00,
144             I_TRACK => 0x01,
145             I_SECTOR => 0x02,
146             I_NAME => 0x03,
147             I_SIDE_TRACK => 0x13,
148             I_SIDE_SECTOR => 0x14,
149             I_RECORD_LENGTH => 0x15,
150             I_SIZE_LO => 0x1c,
151             I_SIZE_HI => 0x1d,
152             );
153              
154             # Other package constant values:
155             my %other_package_constants = (
156             ITEM_SIZE => 0x1e,
157             );
158              
159             # Setup package constant values:
160             my %all_constants = (%file_type_constants, %data_offset_constants, %other_package_constants);
161             while (my ($name, $value) = each %all_constants) {
162             if ($] < 5.008) {
163             eval sprintf q{
164             Readonly \\$%s => %d;
165             }, $name, $value;
166             }
167             else {
168             eval sprintf q{
169             Readonly $%s => %d;
170             }, $name, $value;
171             }
172             }
173              
174 8     8   58 use base qw(Exporter);
  8         17  
  8         11108  
175             our %EXPORT_TAGS = ();
176             $EXPORT_TAGS{'types'} = [ qw($T_DEL $T_SEQ $T_PRG $T_USR $T_REL $T_CBM $T_DIR) ];
177             $EXPORT_TAGS{'all'} = [ @{$EXPORT_TAGS{'types'}} ];
178             our @EXPORT_OK = ( @{$EXPORT_TAGS{'all'}} );
179             our @EXPORT = qw();
180              
181             =head2 new
182              
183             Create an instance of a C class as an empty directory entry:
184              
185             my $item = D64::Disk::Dir::Item->new();
186              
187             Create an instance of a C class providing 30 bytes of data retrieved from a disk directory:
188              
189             my $item = D64::Disk::Dir::Item->new(data => $data);
190             my $item = D64::Disk::Dir::Item->new(data => \@data);
191              
192             =cut
193              
194             sub new {
195 147     147 1 75519 my ($this) = shift;
196 147   66     640 my $class = ref ($this) || $this;
197 147         340 my $object = $class->_init();
198 147         379 my $self = bless $object, $class;
199 147 100       497 $self->data(@_) if @_;
200 141         367 return $self;
201             }
202              
203             sub _init {
204 147     147   182 my ($class) = @_;
205 147         539 my @object = map { chr 0x00 } (0x01 .. $ITEM_SIZE);
  4410         6545  
206 147         605 return \@object;
207             }
208              
209             =head2 data
210              
211             Fetch item data as a scalar of 30 bytes:
212              
213             my $data = $item->data();
214              
215             Fetch item data as an array of 30 bytes:
216              
217             my @data = $item->data();
218              
219             Update item providing 30 bytes of scalar data retrieved from a disk directory:
220              
221             $item->data($data);
222              
223             Update item given array with 30 bytes of data retrieved from a disk directory:
224              
225             $item->data(@data);
226             $item->data(\@data);
227              
228             =cut
229              
230             sub data {
231 132     132 1 2070 my ($self, @args) = @_;
232              
233 132 100       292 if (scalar @args > 0) {
234 112 100       233 if (scalar @args == 1) {
235 17         23 my ($arg) = @args;
236 17 100       43 if (ref $arg eq 'ARRAY') {
237 6         7 @args = @{$arg};
  6         32  
238             }
239             }
240              
241 112 100       364 if (scalar @args == 1) {
    100          
242 11         20 my ($arg) = @args;
243 11 50       25 unless (ref $arg) {
244 11 100       32 unless (length $arg == 30) {
245 4         49 die q{Unable to set directory item data: Invalid length of data};
246             }
247 7         62 @{$self} = split //, $arg;
  7         120  
248             }
249             else {
250 0         0 die q{Unable to set directory item data: Invalid arguments given};
251             }
252             }
253             elsif (scalar @args == 30) {
254 97         205 for (my $i = 0; $i < @args; $i++) {
255 2832         3337 my $byte_value = $args[$i];
256 2832 100       4490 unless ($self->_is_valid_data_type($byte_value)) {
257 2         42 die sprintf q{Invalid data type at offset %d (%s)}, $i, ref $args[$i];
258             }
259 2830 100       4404 unless ($self->_is_valid_byte_value($byte_value)) {
260 2         26 die sprintf q{Invalid byte value at offset %d ($%x)}, $i, $byte_value;
261             }
262             }
263 93         114 @{$self} = @args;
  93         1335  
264             }
265             else {
266 4         43 die q{Unable to set directory item data: Invalid amount of data};
267             }
268             }
269              
270 120 100       638 return unless defined wantarray;
271 23 100       51 return wantarray ? @{$self} : join '', @{$self};
  5         47  
  18         132  
272             }
273              
274             sub _is_valid_data_type {
275 2882     2882   3279 my ($self, $byte_value) = @_;
276              
277 2882 100       5093 unless (ref $byte_value) {
278 2874         8513 return 1;
279             }
280              
281 8         22 return 0;
282             }
283              
284             sub _is_valid_byte_value {
285 2830     2830   4893 my ($self, $byte_value) = @_;
286              
287 2830 50 66     16473 if (length ($byte_value) == 1 && ord ($byte_value) >= 0x00 && ord ($byte_value) <= 0xff) {
      66        
288 2828         9409 return 1;
289             }
290              
291 2         4 return 0;
292             }
293              
294             sub _is_valid_number_value {
295 33     33   45 my ($self, $number_value) = @_;
296              
297 33 100 66     59 if ($self->is_int($number_value) && $number_value >= 0x00 && $number_value <= 0xff) {
      100        
298 23         92 return 1;
299             }
300              
301 10         23 return 0;
302             }
303              
304             sub is_valid_string_value {
305 23     23 0 37 my ($self, $string_value) = @_;
306              
307 8     8   68 no bytes;
  8         15  
  8         75  
308 23 50       85 unless (grep { ord ($_) < 0x00 || ord ($_) > 0xff } split //, $string_value) {
  237 100       765  
309 22         58 return 1;
310             }
311              
312 1         4 return 0;
313             }
314              
315             =head2 bytes
316              
317             C is simply a convenient alias for C.
318              
319             Fetch item data as a scalar of 30 bytes:
320              
321             my $bytes = $item->bytes();
322              
323             Fetch item data as an array of 30 bytes:
324              
325             my @bytes = $item->bytes();
326              
327             Update item providing 30 bytes of scalar data retrieved from a disk directory:
328              
329             $item->bytes($bytes);
330              
331             Update item given array with 30 bytes of data retrieved from a disk directory:
332              
333             $item->bytes(@bytes);
334             $item->bytes(\@bytes);
335              
336             =cut
337              
338             *bytes = \&data;
339              
340             =head2 type
341              
342             Get the actual file type:
343              
344             my $type = $item->type();
345              
346             Set the actual file type:
347              
348             $item->type($type);
349              
350             The following file type constants are the only valid values that may be used to update current item type: C<$T_DEL>, C<$T_SEQ>, C<$T_PRG>, C<$T_USR>, C<$T_REL>, C<$T_CBM>, and C<$T_DIR>.
351              
352             =cut
353              
354             sub type {
355 108     108 1 674 my ($self, $type) = @_;
356              
357 108 100       330 if (defined $type) {
358 41 100       107 if (ref $type) {
359 1         8 die q{Invalid file type constant (scalar value expected)};
360             }
361 40 100       100 unless ($self->is_int($type)) {
362 1         8 die q{Invalid file type constant (type constant expected)};
363             }
364 39 100       118 if ($type - ($type & 0b1111)) {
365 1         8 die q{Invalid file type constant (only bits 0-3 can be set)};
366             }
367 38         119 my @valid_values = (0b000, 0b001, 0b010, 0b011, 0b100, 0b101, 0b110);
368 38 100       96 unless (grep { $_ == $type } @valid_values) {
  266         435  
369 2         23 die q{Illegal file type constant};
370             }
371 36         116 $self->[$I_TYPE] = chr ((ord ($self->[$I_TYPE]) & 0b11110000) | $type);
372             }
373              
374 103         481 return ord ($self->[$I_TYPE]) & 0b1111;
375             }
376              
377             =head2 closed
378              
379             Get "closed" flag:
380              
381             my $is_closed = $item->closed();
382              
383             Returns true when "closed" flag is set, and false otherwise.
384              
385             Set "closed" flag:
386              
387             $item->closed($is_closed);
388              
389             When "closed" flag is not set, it produces "*", or "splat" files.
390              
391             =cut
392              
393             sub closed {
394 28     28 1 158 my ($self, $is_closed) = @_;
395              
396 28 100       75 if (defined $is_closed) {
397 12 100       30 if (ref $is_closed) {
398 1         12 die q{Invalid "closed" flag};
399             }
400 11 100       40 my $closed_bit = $is_closed ? 0b10000000 : 0b00000000;
401 11         36 $self->[$I_CLOSED] = chr ((ord ($self->[$I_CLOSED]) & 0b01111111) | $closed_bit);
402             }
403              
404 27         140 return (ord ($self->[$I_CLOSED]) & 0b10000000) == 0b10000000;
405             }
406              
407             =head2 locked
408              
409             Get "locked" flag:
410              
411             my $is_locked = $item->locked();
412              
413             Returns true when "locked" flag is set, and false otherwise.
414              
415             Set "locked" flag:
416              
417             $item->locked($is_locked);
418              
419             When "locked" flag is set, it produces ">" locked files.
420              
421             =cut
422              
423             sub locked {
424 22     22 1 138 my ($self, $is_locked) = @_;
425              
426 22 100       56 if (defined $is_locked) {
427 10 100       24 if (ref $is_locked) {
428 1         9 die q{Invalid "locked" flag};
429             }
430 9 100       22 my $locked_bit = $is_locked ? 0b01000000 : 0b00000000;
431 9         29 $self->[$I_LOCKED] = chr ((ord ($self->[$I_LOCKED]) & 0b10111111) | $locked_bit);
432             }
433              
434 21         117 return (ord ($self->[$I_LOCKED]) & 0b01000000) == 0b01000000;
435             }
436              
437             =head2 track
438              
439             Get track location of first sector of file:
440              
441             my $track = $item->track();
442              
443             Set track location of first sector of file:
444              
445             $item->track($track);
446              
447             =cut
448              
449             sub track {
450 15     15 1 158 my ($self, $track) = @_;
451              
452 15 100       45 if (defined $track) {
453 9 100       22 unless ($self->_is_valid_data_type($track)) {
454 1         4 die sprintf q{Invalid type (%s) of track location of first sector of file (single byte expected)}, $self->_dump($track);
455             }
456 8 100       23 unless ($self->_is_valid_number_value($track)) {
457 2         6 die sprintf q{Invalid value (%s) of track location of first sector of file (single byte expected)}, $self->_dump($track);
458             }
459 6         41 $self->[$I_TRACK] = pack 'C', $track;
460             }
461              
462 12         55 return unpack 'C', $self->[$I_TRACK];
463             }
464              
465             =head2 sector
466              
467             Get sector location of first sector of file:
468              
469             my $sector = $item->sector();
470              
471             Set sector location of first sector of file:
472              
473             $item->sector($sector);
474              
475             =cut
476              
477             sub sector {
478 15     15 1 174 my ($self, $sector) = @_;
479              
480 15 100       39 if (defined $sector) {
481 9 100       27 unless ($self->_is_valid_data_type($sector)) {
482 1         4 die sprintf q{Invalid type (%s) of sector location of first sector of file (single byte expected)}, $self->_dump($sector);
483             }
484 8 100       20 unless ($self->_is_valid_number_value($sector)) {
485 2         6 die sprintf q{Invalid value (%s) of sector location of first sector of file (single byte expected)}, $self->_dump($sector);
486             }
487 6         26 $self->[$I_SECTOR] = pack 'C', $sector;
488             }
489              
490 12         55 return unpack 'C', $self->[$I_SECTOR];
491             }
492              
493             =head2 name
494              
495             Get 16 character filename:
496              
497             my $name = $item->name();
498              
499             Returned value is a CBM ASCII string. Unless specified otherwise, it will be padded with C<$A0>.
500              
501             Get filename (without C<$A0> padding):
502              
503             my $name = $item->name(padding_with_a0 => 0);
504              
505             C input parameter defaults to C<1>. That means every time filename is fetched from a C object, length of a retrieved string will be 16 characters.
506              
507             Set 16 character filename:
508              
509             $item->name($name);
510              
511             Input name parameter is expected to be CBM ASCII string. Unless specified otherwise, it will be padded with C<$A0>.
512              
513             Set 16 character filename (without C<$A0> padding):
514              
515             $item->name($name, padding_with_a0 => 0);
516              
517             C input parameter defaults to C<1>. That means every time filename is written into a C object, it gets complemented with additional C<$A0> bytes up to the maximum length of a filename, which is 16 bytes. Thus by default 16 characters of filename data are always stored in a disk directory item.
518              
519             In order to convert a PETSCII string to an ASCII string and vice versa, use the following subroutines provided by C module:
520              
521             use Text::Convert::PETSCII qw/:all/;
522              
523             my $ascii_name = petscii_to_ascii($petscii_name);
524             my $petscii_name = ascii_to_petscii($ascii_name);
525              
526             See L module description for more details on ASCII/PETSCII text conversion.
527              
528             =cut
529              
530             sub name {
531 97     97 1 594 my ($self, @options) = @_;
532              
533 97         96 my $name;
534 97 100       248 if (scalar (@options) % 2 == 1) {
535 26         36 $name = shift @options;
536             }
537 97         190 my %options = @options;
538 97 100       224 $options{padding_with_a0} = 1 if not exists $options{padding_with_a0};
539              
540 97 100       178 if (defined $name) {
541 26 100       65 unless ($self->is_str($name)) {
542 2         8 die sprintf q{Invalid type (%s) of filename (string value expected)}, $self->_dump($name);
543             }
544 24 100       54 if (length $name > 16) {
545 1         3 die sprintf q{Too long (%s) filename (maximum 16 PETSCII characters allowed)}, $self->_dump($name);
546             }
547 23 100       60 unless ($self->is_valid_string_value($name)) {
548 1         3 die sprintf q{Invalid string (%s) of filename (PETSCII string expected)}, $self->_dump($name);
549             }
550 22 100       119 if ($options{padding_with_a0}) {
551 13         61 $self->[$I_NAME + $_] = chr 0xa0 for (0 .. 15);
552             }
553 22         780 for (my $i = 0; $i < length $name; $i++) {
554 233         1335 $self->[$I_NAME + $i] = substr $name, $i, 1;
555             }
556             }
557              
558 93         302 my $name_length = $I_NAME + 15;
559 93 100       488 unless ($options{padding_with_a0}) {
560 60   66     356 while (ord ($self->[$name_length]) == 0xa0 && $name_length >= $I_NAME) {
561 579         4193 $name_length--;
562             }
563             }
564 93         216 $name = join '', @{$self}[$I_NAME..$name_length];
  93         522  
565              
566 93         409 return $name;
567             }
568              
569             =head2 side_track
570              
571             Get track location of first side-sector block:
572              
573             my $side_track = $item->side_track();
574              
575             A track location of first side-sector block is returned for relative files only, an undefined value otherwise.
576              
577             Set track location of first side-sector block:
578              
579             $item->side_track($side_track);
580              
581             When attempting to assign track location of first side-sector block for a non-relative file, an exception will be thrown.
582              
583             =cut
584              
585             sub side_track {
586 12     12 1 183 my ($self, $side_track) = @_;
587              
588 12 100       31 if (defined $side_track) {
589 7 100       13 unless ($self->type() eq $T_REL) {
590 1         10 die sprintf q{Illegal file type ('%s') encountered when attempting to set track location of first side-sector block ('rel' files only)}, $self->type_to_string($self->type());
591             }
592 6 100       56 unless ($self->_is_valid_data_type($side_track)) {
593 1         4 die sprintf q{Invalid type (%s) of track location of first side-sector block of file (single byte expected)}, $self->_dump($side_track);
594             }
595 5 100       19 unless ($self->_is_valid_number_value($side_track)) {
596 2         6 die sprintf q{Invalid value (%s) of track location of first side-sector block of file (single byte expected)}, $self->_dump($side_track);
597             }
598 3         16 $self->[$I_SIDE_TRACK] = pack 'C', $side_track;
599             }
600              
601 8 100       26 return unless $self->type() eq $T_REL;
602              
603 7         71 return unpack 'C', $self->[$I_SIDE_TRACK];
604             }
605              
606             =head2 side_sector
607              
608             Get sector location of first side-sector block:
609              
610             my $side_sector = $item->side_sector();
611              
612             A sector location of first side-sector block is returned for relative files only, an undefined value otherwise.
613              
614             Set sector location of first side-sector block:
615              
616             $item->side_sector($side_sector);
617              
618             When attempting to assign sector location of first side-sector block for a non-relative file, an exception will be thrown.
619              
620             =cut
621              
622             sub side_sector {
623 12     12 1 213 my ($self, $side_sector) = @_;
624              
625 12 100       29 if (defined $side_sector) {
626 7 100       17 unless ($self->type() eq $T_REL) {
627 1         10 die sprintf q{Illegal file type ('%s') encountered when attempting to set sector location of first side-sector block ('rel' files only)}, $self->type_to_string($self->type());
628             }
629 6 100       57 unless ($self->_is_valid_data_type($side_sector)) {
630 1         3 die sprintf q{Invalid type (%s) of sector location of first side-sector block of file (single byte expected)}, $self->_dump($side_sector);
631             }
632 5 100       14 unless ($self->_is_valid_number_value($side_sector)) {
633 2         6 die sprintf q{Invalid value (%s) of sector location of first side-sector block of file (single byte expected)}, $self->_dump($side_sector);
634             }
635 3         14 $self->[$I_SIDE_SECTOR] = pack 'C', $side_sector;
636             }
637              
638 8 100       31 return unless $self->type() eq $T_REL;
639              
640 7         67 return unpack 'C', $self->[$I_SIDE_SECTOR];
641             }
642              
643             =head2 record_length
644              
645             Get relative file record length:
646              
647             my $record_length = $item->record_length();
648              
649             A relative file record length is returned for relative files only, an undefined value otherwise.
650              
651             Get relative file record length (relative file only, maximum value 254):
652              
653             $item->record_length($record_length);
654              
655             When attempting to assign relative file record length for a non-relative file or a record length greater than 254, an exception will be thrown.
656              
657             =cut
658              
659             sub record_length {
660 15     15 1 355 my ($self, $record_length) = @_;
661              
662 15 100       34 if (defined $record_length) {
663 9 100       21 unless ($self->type() eq $T_REL) {
664 1         11 die sprintf q{Illegal file type ('%s') encountered when attempting to set record length ('rel' files only)}, $self->type_to_string($self->type());
665             }
666 8 100       124 unless ($self->_is_valid_data_type($record_length)) {
667 1         4 die sprintf q{Invalid type (%s) of relative file record length (single byte expected)}, $self->_dump($record_length);
668             }
669 7 100       14 unless ($self->_is_valid_number_value($record_length)) {
670 2         4 die sprintf q{Invalid value (%s) of relative file record length (single byte expected)}, $self->_dump($record_length);
671             }
672 5 100 66     29 unless ($record_length >= 0x00 && $record_length < 0xff) {
673 1         5 die sprintf q{Invalid value (%s) of relative file record length (maximum allowed value 254)}, $self->_dump($record_length);
674             }
675 4         19 $self->[$I_RECORD_LENGTH] = pack 'C', $record_length;
676             }
677              
678 10 100       65 return unless $self->type() eq $T_REL;
679              
680 9         107 return unpack 'C', $self->[$I_RECORD_LENGTH];
681             }
682              
683             =head2 size
684              
685             Get file size in sectors:
686              
687             my $size = $item->size();
688              
689             The approximate file size in bytes is <= number_of_sectors * 254.
690              
691             Set file size in sectors:
692              
693             $item->size($size);
694              
695             =cut
696              
697             sub size {
698 24     24 1 199 my ($self, $size) = @_;
699              
700 24 100       55 if (defined $size) {
701 12 100 100     34 unless ($self->_is_valid_data_type($size) && $self->is_int($size)) {
702 2         5 die sprintf q{Invalid type (%s) of file size (integer value expected)}, $self->_dump($size);
703             }
704 10 100 66     62 unless ($size >= 0x0000 && $size <= 0xffff) {
705 1         3 die sprintf q{Invalid value (%s) of file size (maximum allowed value %d)}, $self->_dump($size), 0xffff;
706             }
707              
708 9         17 my $size_lo = $size % 0x0100;
709 9         22 my $size_hi = int($size / 0x0100);
710              
711 9         75 $self->[$I_SIZE_LO] = pack 'C', $size_lo;
712 9         58 $self->[$I_SIZE_HI] = pack 'C', $size_hi;
713             }
714              
715 21         85 my $size_lo = unpack 'C', $self->[$I_SIZE_LO];
716 21         144 my $size_hi = unpack 'C', $self->[$I_SIZE_HI];
717              
718             # Since a scalar value of a double type (NV) will always be loaded as the result
719             # of multiplication in Perl 5.6.2, we need to force an integer value into an SV:
720 21         134 return $self->set_iok($size_lo + 256 * $size_hi);
721             }
722              
723             =head2 exact_size
724              
725             Get exact file size in bytes:
726              
727             my $exact_size = $item->exact_size(disk_image => $disk_image_ref);
728              
729             Warning! Do not use! This method has not been implemented (yet)!
730              
731             =cut
732              
733             sub exact_size {
734 0     0 1 0 my ($self) = @_;
735              
736             # TODO: add another input parameter: required provision of a D64 disk image data...
737              
738 0         0 die q{Not yet implemented};
739             }
740              
741             =head2 print
742              
743             Print out formatted disk image directory item:
744              
745             $item->print(fh => $fh, as_petscii => $as_petscii);
746              
747             C defaults to the standard output. C defaults to false (meaning that ASCII characters will be printed out by default).
748              
749             =cut
750              
751             sub print {
752 4     4 1 34 my ($self, %args) = @_;
753              
754 4         7 my $fh = $args{fh};
755 4         5 my $as_petscii = $args{as_petscii};
756              
757 4   33     16 $fh ||= *STDOUT;
758 4         28 $fh->binmode(':bytes');
759              
760 4         20 my $stdout = select $fh;
761              
762 4 100       9 if ($as_petscii) {
763 2         6 my $type = $self->type_to_string($self->type(), 1);
764 2 100       6 my $closed = $self->closed() ? 0x20 : 0x2a; # "*"
765 2 100       14 my $locked = $self->locked() ? 0x3c : 0x20; # "<"
766 2         16 my $size = ascii_to_petscii($self->size());
767 2         55 my $name = sprintf "\"%s\"", $self->name(padding_with_a0 => 0);
768 2         5 $name =~ s/\x00//g; # align file type string to the right column
769 2         7 printf "%-4d %-18s%c%s%c\n", $size, $name, $closed, $type, $locked;
770             }
771             else {
772 2         8 my $type = $self->type_to_string($self->type());
773 2 100       7 my $closed = $self->closed() ? ord ' ' : ord '*';
774 2 100       19 my $locked = $self->locked() ? ord '<' : ord ' ';
775 2         18 my $size = $self->size();
776 2         7 my $name = sprintf "\"%s\"", petscii_to_ascii($self->name(padding_with_a0 => 0));
777 2         216 $name =~ s/\x00//g; # align file type string to the right column
778 2         17 printf "%-4d %-18s%c%s%c\n", $size, $name, $closed, $type, $locked;
779             }
780              
781 4         87 select $stdout;
782              
783 4         12 return;
784             }
785              
786             =head2 validate
787              
788             Validate item data against all possible errors:
789              
790             my $is_valid = $item->validate();
791              
792             Returns true when all item data is valid, and false otherwise.
793              
794             =cut
795              
796             sub validate {
797 3     3 1 147 my ($self) = @_;
798              
799 3         10 my $test = $self->new();
800              
801             my $is_valid = try {
802 3     3   212 my $data = $self->data();
803 3 50       28 die unless defined $data;
804 3         11 $test->data($data);
805              
806 3         13 my $type = $self->type();
807 3 50       24 die unless defined $type;
808 3         7 $test->type($type);
809              
810 2         12 my $closed = $self->closed();
811 2 50       13 die unless defined $closed;
812 2         5 $test->closed($closed);
813              
814 2         12 my $locked = $self->locked();
815 2 50       20 die unless defined $locked;
816 2         7 $test->locked($locked);
817              
818 2         12 my $track = $self->track();
819 2 50       25 die unless defined $track;
820 2         5 $test->track($track);
821              
822 2         18 my $sector = $self->sector();
823 2 50       14 die unless defined $sector;
824 2         5 $test->sector($sector);
825              
826 2         14 my $name = $self->name();
827 2 50       14 die unless defined $name;
828 2         6 $test->name($name);
829              
830 2 50       7 if ($self->type() eq $T_REL) {
831              
832 0         0 my $side_track = $self->side_track();
833 0 0       0 die unless defined $side_track;
834 0         0 $test->side_track($side_track);
835              
836 0         0 my $side_sector = $self->side_sector();
837 0 0       0 die unless defined $side_sector;
838 0         0 $test->side_sector($side_sector);
839              
840 0         0 my $record_length = $self->record_length();
841 0 0       0 die unless defined $record_length;
842 0         0 $test->record_length($record_length);
843             }
844              
845 2         27 my $size = $self->size();
846 2 50       7 die unless defined $size;
847 2         6 $test->size($size);
848              
849 2         9 1;
850             }
851             catch {
852 1     1   18 0;
853 3         101 };
854              
855 3         100 return $is_valid;
856             }
857              
858             =head2 empty
859              
860             Check if directory item contains information about the actual disk file:
861              
862             my $is_empty = $item->empty();
863              
864             True value will be returned when directory item object is empty.
865              
866             =cut
867              
868             sub empty {
869 2     2 1 18 my ($self) = @_;
870              
871 2         4 my $is_empty = not grep { ord ($_) != 0x00 } @{$self};
  60         101  
  2         4  
872              
873 2         7 return $is_empty;
874             }
875              
876             =head2 writable
877              
878             Check if slot occupied by this item in a disk directory is writable and can be replaced by any new file that would be written into disk:
879              
880             my $is_writable = $item->writable();
881              
882             True value will be returned when directory item object is writable.
883              
884             =cut
885              
886             sub writable {
887 4     4 1 32 my ($self) = @_;
888              
889 4   66     10 my $is_writable = !$self->closed() && $self->type() eq $T_DEL;
890              
891 4         39 return $is_writable;
892             }
893              
894             =head2 clone
895              
896             Clone disk directory item:
897              
898             my $clone = $item->clone();
899              
900             =head2 match_name
901              
902             Check if filename matches given CBM ASCII pattern:
903              
904             my $is_matched = $item->match_name($petscii_pattern);
905              
906             C<$petscii_pattern> is expected to be a CBM ASCII string containing optional wildcard characters. The following wildcards are allowed/recognized:
907              
908             =over
909              
910             =item *
911             An asterisk C<*> character following any program name will yield successful match if filename is starting with that name.
912              
913             =item *
914             A question mark C character used as a wildcard will match any character in a filename.
915              
916             =back
917              
918             =cut
919              
920             sub match_name {
921 29     29 1 12263 my ($self, $petscii_pattern) = @_;
922              
923 29         65 my $name = $self->name(padding_with_a0 => 0);
924              
925 29         117 my @name = split //, $name;
926 29         90 my @pattern = split //, $petscii_pattern;
927              
928 29         82 for (my $i = 0; $i < @pattern; $i++) {
929 135         152 my $match_pattern = ord $pattern[$i];
930 135 100       251 if ($match_pattern == 0x2a) {
931 14         83 return 1;
932             }
933 121         160 my $character = $name[$i];
934 121 100 100     443 unless (defined $character && $match_pattern == 0x3f) {
935 110 100 100     604 if (!defined $character || ord $character != $match_pattern) {
936 4         29 return 0;
937             }
938             }
939             }
940              
941 11 100       25 if (@name == @pattern) {
942 7         50 return 1;
943             }
944              
945 4         22 return 0;
946             }
947              
948             =head2 type_to_string
949              
950             Convert given file type into its three-letter printable ASCII/PETSCII string representation:
951              
952             my $string = D64::Disk::Dir::Item->type_to_string($type, $as_petscii);
953              
954             C defaults to false (meaning that ASCII characters will be returned by default).
955              
956             =cut
957              
958             sub type_to_string {
959 23     23 1 4877 my ($this, $type, $as_petscii) = @_;
960              
961 23 100       90 unless ($as_petscii) {
962 13         33 my @mapping = (
963             'del', # $T_DEL
964             'seq', # $T_SEQ
965             'prg', # $T_PRG
966             'usr', # $T_USR
967             'rel', # $T_REL
968             'cbm', # $T_CBM
969             'dir', # $T_DIR
970             );
971              
972 13 100 66     69 if ($type >= 0 && $type < @mapping) {
973 12         88 return $mapping[$type]
974             }
975             else {
976 1         4 return '???';
977             }
978             }
979             else {
980 10         29 my @mapping = (
981             '44454c', # $T_DEL
982             '534551', # $T_SEQ
983             '505247', # $T_PRG
984             '555352', # $T_USR
985             '52454c', # $T_REL
986             '43424d', # $T_CBM
987             '444952', # $T_DIR
988             );
989              
990 10 100 66     48 if ($type >= 0 && $type < @mapping) {
991 9         45 return pack 'H*', $mapping[$type];
992             }
993             else {
994 1         5 return pack 'H*', '3f3f3f';
995             }
996             }
997             }
998              
999             sub _dump {
1000 23     23   30 my ($self, $value) = @_;
1001              
1002 23         102 my $dump = Data::Dumper->new([$value])->Indent(0)->Terse(1)->Deepcopy(1)->Sortkeys(1)->Dump();
1003              
1004 23         1806 return $dump;
1005             }
1006              
1007             sub is_int {
1008 91     91 0 1536 my ($this, $var) = @_;
1009              
1010 91         443 return _is_int($var);
1011             }
1012              
1013             sub is_str {
1014 35     35 0 1953 my ($this, $var) = @_;
1015              
1016 35         121 return _is_str($var);
1017             }
1018              
1019             sub magic_to_int {
1020 2     2 0 894 my ($this, $magic) = @_;
1021              
1022 2         15 return _magic_to_int($magic);
1023             }
1024              
1025             sub set_iok {
1026 21     21 0 33 my ($self, $var) = @_;
1027              
1028 21         57 my $var_iok = _set_iok($var);
1029              
1030 21         62 return $var_iok;
1031             }
1032              
1033             =head1 BUGS
1034              
1035             There are no known bugs at the moment. Please report any bugs or feature requests.
1036              
1037             =head1 CAVEATS
1038              
1039             No GEOS-specific properties are supported by accessor methods of this module. Due to low popularity of GEOS system and rare amount of GEOS D64 disk images available on the net, I have decided to intentionally skip implementation of C file type format here. Thus all the information needed for the windowing system (icon, window position, creation time/date) cannot be right now accessed conveniently without the knowledge of specific C format details.
1040              
1041             =head1 EXPORT
1042              
1043             C exports nothing by default.
1044              
1045             You may request the import of file type constants (C<$T_DEL>, C<$T_SEQ>, C<$T_PRG>, C<$T_USR>, C<$T_REL>, C<$T_CBM>, and C<$T_DIR>) individually. All of these constants can be explicitly imported from C by using it with the ":types" tag. All constants can be explicitly imported from C by using it with the ":all" tag.
1046              
1047             =head1 SEE ALSO
1048              
1049             L, L, L.
1050              
1051             =head1 AUTHOR
1052              
1053             Pawel Krol, Epawelkrol@cpan.orgE.
1054              
1055             =head1 VERSION
1056              
1057             Version 0.07 (2013-03-08)
1058              
1059             =head1 COPYRIGHT AND LICENSE
1060              
1061             Copyright 2013 by Pawel Krol .
1062              
1063             This library is free open source software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.6 or, at your option, any later version of Perl 5 you may have available.
1064              
1065             PLEASE NOTE THAT IT COMES WITHOUT A WARRANTY OF ANY KIND!
1066              
1067             =cut
1068              
1069             1;