File Coverage

blib/lib/D64/Disk/Dir/Item.pm
Criterion Covered Total %
statement 313 326 96.0
branch 155 174 89.0
condition 33 45 73.3
subroutine 42 43 97.6
pod 19 24 79.1
total 562 612 91.8


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   659308 use bytes;
  8         193  
  8         46  
97 8     8   246 use strict;
  8         16  
  8         151  
98 8     8   4725 use utf8;
  8         102  
  8         40  
99 8     8   231 use warnings;
  8         30  
  8         396  
100              
101             our $VERSION = '0.08';
102              
103 8     8   3401 use parent 'Clone';
  8         2501  
  8         52  
104              
105 8     8   28028 use Data::Dumper;
  8         45979  
  8         482  
106 8     8   3790 use Readonly;
  8         28344  
  8         428  
107 8     8   62 use Scalar::Util qw(looks_like_number);
  8         21  
  8         464  
108 8     8   3928 use Text::Convert::PETSCII qw(:convert);
  8         16934  
  8         1304  
109 8     8   4314 use Try::Tiny;
  8         16858  
  8         2460  
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   69 use base qw(Exporter);
  8         18  
  8         6317  
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 149     149 1 82913 my ($this) = shift;
196 149   66     679 my $class = ref ($this) || $this;
197 149         348 my $object = $class->_init();
198 149         285 my $self = bless $object, $class;
199 149 100       502 $self->data(@_) if @_;
200 143         341 return $self;
201             }
202              
203             sub _init {
204 149     149   262 my ($class) = @_;
205 149         527 my @object = map { chr 0x00 } (0x01 .. $ITEM_SIZE);
  4470         7509  
206 149         465 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 134     134 1 2452 my ($self, @args) = @_;
232              
233 134 100       346 if (scalar @args > 0) {
234 114 100       308 if (scalar @args == 1) {
235 17         31 my ($arg) = @args;
236 17 100       76 if (ref $arg eq 'ARRAY') {
237 6         9 @args = @{$arg};
  6         24  
238             }
239             }
240              
241 114 100       306 if (scalar @args == 1) {
    100          
242 11         21 my ($arg) = @args;
243 11 50       24 unless (ref $arg) {
244 11 100       36 unless (length $arg == 30) {
245 4         41 die q{Unable to set directory item data: Invalid length of data};
246             }
247 7         13 @{$self} = split //, $arg;
  7         62  
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 99         249 for (my $i = 0; $i < @args; $i++) {
255 2892         4088 my $byte_value = $args[$i];
256 2892 100       4460 unless ($self->_is_valid_data_type($byte_value)) {
257 2         57 die sprintf q{Invalid data type at offset %d (%s)}, $i, ref $args[$i];
258             }
259 2890 100       4523 unless ($self->_is_valid_byte_value($byte_value)) {
260 2         28 die sprintf q{Invalid byte value at offset %d ($%x)}, $i, $byte_value;
261             }
262             }
263 95         155 @{$self} = @args;
  95         912  
264             }
265             else {
266 4         52 die q{Unable to set directory item data: Invalid amount of data};
267             }
268             }
269              
270 122 100       470 return unless defined wantarray;
271 23 100       48 return wantarray ? @{$self} : join '', @{$self};
  5         41  
  18         107  
272             }
273              
274             sub _is_valid_data_type {
275 2943     2943   4284 my ($self, $byte_value) = @_;
276              
277 2943 100       4830 unless (ref $byte_value) {
278 2935         5318 return 1;
279             }
280              
281 8         30 return 0;
282             }
283              
284             sub _is_valid_byte_value {
285 2890     2890   4368 my ($self, $byte_value) = @_;
286              
287 2890 50 66     10310 if (length ($byte_value) == 1 && ord ($byte_value) >= 0x00 && ord ($byte_value) <= 0xff) {
      66        
288 2888         7186 return 1;
289             }
290              
291 2         5 return 0;
292             }
293              
294             sub _is_valid_number_value {
295 33     33   53 my ($self, $number_value) = @_;
296              
297 33 100 66     65 if ($self->is_int($number_value) && $number_value >= 0x00 && $number_value <= 0xff) {
      100        
298 23         69 return 1;
299             }
300              
301 10         24 return 0;
302             }
303              
304             sub is_valid_string_value {
305 24     24 0 51 my ($self, $string_value) = @_;
306              
307 8     8   62 no bytes;
  8         26  
  8         68  
308 24 50       108 unless (grep { ord ($_) < 0x00 || ord ($_) > 0xff } split //, $string_value) {
  252 100       657  
309 23         66 return 1;
310             }
311              
312 1         5 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 111     111 1 862 my ($self, $type) = @_;
356              
357 111 100       346 if (defined $type) {
358 42 100       92 if (ref $type) {
359 1         11 die q{Invalid file type constant (scalar value expected)};
360             }
361 41 100       114 unless ($self->is_int($type)) {
362 1         9 die q{Invalid file type constant (type constant expected)};
363             }
364 40 100       114 if ($type - ($type & 0b1111)) {
365 1         10 die q{Invalid file type constant (only bits 0-3 can be set)};
366             }
367 39         82 my @valid_values = (0b000, 0b001, 0b010, 0b011, 0b100, 0b101, 0b110);
368 39 100       66 unless (grep { $_ == $type } @valid_values) {
  273         482  
369 2         31 die q{Illegal file type constant};
370             }
371 37         103 $self->[$I_TYPE] = chr ((ord ($self->[$I_TYPE]) & 0b11110000) | $type);
372             }
373              
374 106         499 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 31     31 1 200 my ($self, $is_closed) = @_;
395              
396 31 100       67 if (defined $is_closed) {
397 13 100       33 if (ref $is_closed) {
398 1         9 die q{Invalid "closed" flag};
399             }
400 12 100       50 my $closed_bit = $is_closed ? 0b10000000 : 0b00000000;
401 12         36 $self->[$I_CLOSED] = chr ((ord ($self->[$I_CLOSED]) & 0b01111111) | $closed_bit);
402             }
403              
404 30         180 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 25     25 1 234 my ($self, $is_locked) = @_;
425              
426 25 100       53 if (defined $is_locked) {
427 11 100       25 if (ref $is_locked) {
428 1         9 die q{Invalid "locked" flag};
429             }
430 10 100       36 my $locked_bit = $is_locked ? 0b01000000 : 0b00000000;
431 10         30 $self->[$I_LOCKED] = chr ((ord ($self->[$I_LOCKED]) & 0b10111111) | $locked_bit);
432             }
433              
434 24         120 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 21     21 1 279 my ($self, $track) = @_;
451              
452 21 100       55 if (defined $track) {
453 9 100       20 unless ($self->_is_valid_data_type($track)) {
454 1         10 die sprintf q{Invalid type (%s) of track location of first sector of file (single byte expected)}, $self->_dump($track);
455             }
456 8 100       28 unless ($self->_is_valid_number_value($track)) {
457 2         10 die sprintf q{Invalid value (%s) of track location of first sector of file (single byte expected)}, $self->_dump($track);
458             }
459 6         39 $self->[$I_TRACK] = pack 'C', $track;
460             }
461              
462 18         72 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 21     21 1 295 my ($self, $sector) = @_;
479              
480 21 100       50 if (defined $sector) {
481 9 100       18 unless ($self->_is_valid_data_type($sector)) {
482 1         7 die sprintf q{Invalid type (%s) of sector location of first sector of file (single byte expected)}, $self->_dump($sector);
483             }
484 8 100       25 unless ($self->_is_valid_number_value($sector)) {
485 2         10 die sprintf q{Invalid value (%s) of sector location of first sector of file (single byte expected)}, $self->_dump($sector);
486             }
487 6         27 $self->[$I_SECTOR] = pack 'C', $sector;
488             }
489              
490 18         66 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 100     100 1 740 my ($self, @options) = @_;
532              
533 100         135 my $name;
534 100 100       265 if (scalar (@options) % 2 == 1) {
535 27         48 $name = shift @options;
536             }
537 100         205 my %options = @options;
538 100 100       236 $options{padding_with_a0} = 1 if not exists $options{padding_with_a0};
539              
540 100 100       219 if (defined $name) {
541 27 100       57 unless ($self->is_str($name)) {
542 2         6 die sprintf q{Invalid type (%s) of filename (string value expected)}, $self->_dump($name);
543             }
544 25 100       103 if (length $name > 16) {
545 1         3 die sprintf q{Too long (%s) filename (maximum 16 PETSCII characters allowed)}, $self->_dump($name);
546             }
547 24 100       61 unless ($self->is_valid_string_value($name)) {
548 1         12 die sprintf q{Invalid string (%s) of filename (PETSCII string expected)}, $self->_dump($name);
549             }
550 23 100       64 if ($options{padding_with_a0}) {
551 14         65 $self->[$I_NAME + $_] = chr 0xa0 for (0 .. 15);
552             }
553 23         873 for (my $i = 0; $i < length $name; $i++) {
554 248         1286 $self->[$I_NAME + $i] = substr $name, $i, 1;
555             }
556             }
557              
558 96         308 my $name_length = $I_NAME + 15;
559 96 100       491 unless ($options{padding_with_a0}) {
560 62   66     221 while (ord ($self->[$name_length]) == 0xa0 && $name_length >= $I_NAME) {
561 592         3520 $name_length--;
562             }
563             }
564 96         202 $name = join '', @{$self}[$I_NAME..$name_length];
  96         531  
565              
566 96         350 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 343 my ($self, $side_track) = @_;
587              
588 12 100       32 if (defined $side_track) {
589 7 100       14 unless ($self->type() eq $T_REL) {
590 1         28 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       73 unless ($self->_is_valid_data_type($side_track)) {
593 1         11 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       15 unless ($self->_is_valid_number_value($side_track)) {
596 2         8 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         28 $self->[$I_SIDE_TRACK] = pack 'C', $side_track;
599             }
600              
601 8 100       62 return unless $self->type() eq $T_REL;
602              
603 7         70 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 320 my ($self, $side_sector) = @_;
624              
625 12 100       31 if (defined $side_sector) {
626 7 100       14 unless ($self->type() eq $T_REL) {
627 1         11 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       66 unless ($self->_is_valid_data_type($side_sector)) {
630 1         6 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         11 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         26 $self->[$I_SIDE_SECTOR] = pack 'C', $side_sector;
636             }
637              
638 8 100       32 return unless $self->type() eq $T_REL;
639              
640 7         86 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 338 my ($self, $record_length) = @_;
661              
662 15 100       39 if (defined $record_length) {
663 9 100       17 unless ($self->type() eq $T_REL) {
664 1         13 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       82 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       16 unless ($self->_is_valid_number_value($record_length)) {
670 2         15 die sprintf q{Invalid value (%s) of relative file record length (single byte expected)}, $self->_dump($record_length);
671             }
672 5 100 66     26 unless ($record_length >= 0x00 && $record_length < 0xff) {
673 1         7 die sprintf q{Invalid value (%s) of relative file record length (maximum allowed value 254)}, $self->_dump($record_length);
674             }
675 4         21 $self->[$I_RECORD_LENGTH] = pack 'C', $record_length;
676             }
677              
678 10 100       94 return unless $self->type() eq $T_REL;
679              
680 9         88 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 27     27 1 316 my ($self, $size) = @_;
699              
700 27 100       63 if (defined $size) {
701 13 100 100     28 unless ($self->_is_valid_data_type($size) && $self->is_int($size)) {
702 2         9 die sprintf q{Invalid type (%s) of file size (integer value expected)}, $self->_dump($size);
703             }
704 11 100 66     50 unless ($size >= 0x0000 && $size <= 0xffff) {
705 1         8 die sprintf q{Invalid value (%s) of file size (maximum allowed value %d)}, $self->_dump($size), 0xffff;
706             }
707              
708 10         20 my $size_lo = $size % 0x0100;
709 10         31 my $size_hi = int($size / 0x0100);
710              
711 10         60 $self->[$I_SIZE_LO] = pack 'C', $size_lo;
712 10         64 $self->[$I_SIZE_HI] = pack 'C', $size_hi;
713             }
714              
715 24         88 my $size_lo = unpack 'C', $self->[$I_SIZE_LO];
716 24         136 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 24         142 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, verbose => $verbose);
746              
747             C defaults to the standard output. C defaults to false (meaning that ASCII characters will be printed out by default). C defaults to false (changing it to true will additionally print out file's track and sector values).
748              
749             =cut
750              
751             sub print {
752 6     6 1 54 my ($self, %args) = @_;
753              
754 6         10 my $fh = $args{fh};
755 6         11 my $as_petscii = $args{as_petscii};
756 6         8 my $verbose = $args{verbose};
757              
758 6   33     26 $fh ||= *STDOUT;
759 6         53 $fh->binmode(':bytes');
760              
761 6         33 my $stdout = select $fh;
762              
763 6 100       15 if ($as_petscii) {
764 2         5 my $type = $self->type_to_string($self->type(), 1);
765 2 100       5 my $closed = $self->closed() ? 0x20 : 0x2a; # "*"
766 2 100       15 my $locked = $self->locked() ? 0x3c : 0x20; # "<"
767 2         17 my $track = sprintf "%2d", $self->track();
768 2         17 my $sector = sprintf "%2d", $self->sector();
769 2         13 my $size = ascii_to_petscii($self->size());
770 2         78 my $name = sprintf "\"%s\"", $self->name(padding_with_a0 => 0);
771 2         7 $name =~ s/\x00//g; # align file type string to the right column
772 2 50       5 if ($verbose) {
773 0         0 printf "%-4d %-18s%c%s%c %s %s\n", $size, $name, $closed, $type, $locked, $track, $sector;
774             }
775             else {
776 2         8 printf "%-4d %-18s%c%s%c\n", $size, $name, $closed, $type, $locked;
777             }
778             }
779             else {
780 4         12 my $type = $self->type_to_string($self->type());
781 4 100       9 my $closed = $self->closed() ? ord ' ' : ord '*';
782 4 100       28 my $locked = $self->locked() ? ord '<' : ord ' ';
783 4         41 my $track = sprintf "%2d", petscii_to_ascii $self->track();
784 4         191 my $sector = sprintf "%2d", petscii_to_ascii $self->sector();
785 4         104 my $size = $self->size();
786 4         20 my $name = sprintf "\"%s\"", petscii_to_ascii($self->name(padding_with_a0 => 0));
787 4         458 $name =~ s/\x00//g; # align file type string to the right column
788 4 100       14 if ($verbose) {
789 2         9 printf "%-4d %-18s%c%s%c %s %s\n", $size, $name, $closed, $type, $locked, $track, $sector;
790             }
791             else {
792 2         10 printf "%-4d %-18s%c%s%c\n", $size, $name, $closed, $type, $locked;
793             }
794             }
795              
796 6         120 select $stdout;
797              
798 6         17 return;
799             }
800              
801             =head2 validate
802              
803             Validate item data against all possible errors:
804              
805             my $is_valid = $item->validate();
806              
807             Returns true when all item data is valid, and false otherwise.
808              
809             =cut
810              
811             sub validate {
812 3     3 1 112 my ($self) = @_;
813              
814 3         8 my $test = $self->new();
815              
816             my $is_valid = try {
817 3     3   238 my $data = $self->data();
818 3 50       8 die unless defined $data;
819 3         7 $test->data($data);
820              
821 3         10 my $type = $self->type();
822 3 50       22 die unless defined $type;
823 3         7 $test->type($type);
824              
825 2         23 my $closed = $self->closed();
826 2 50       17 die unless defined $closed;
827 2         6 $test->closed($closed);
828              
829 2         10 my $locked = $self->locked();
830 2 50       12 die unless defined $locked;
831 2         5 $test->locked($locked);
832              
833 2         12 my $track = $self->track();
834 2 50       22 die unless defined $track;
835 2         7 $test->track($track);
836              
837 2         16 my $sector = $self->sector();
838 2 50       13 die unless defined $sector;
839 2         6 $test->sector($sector);
840              
841 2         18 my $name = $self->name();
842 2 50       7 die unless defined $name;
843 2         6 $test->name($name);
844              
845 2 50       4 if ($self->type() eq $T_REL) {
846              
847 0         0 my $side_track = $self->side_track();
848 0 0       0 die unless defined $side_track;
849 0         0 $test->side_track($side_track);
850              
851 0         0 my $side_sector = $self->side_sector();
852 0 0       0 die unless defined $side_sector;
853 0         0 $test->side_sector($side_sector);
854              
855 0         0 my $record_length = $self->record_length();
856 0 0       0 die unless defined $record_length;
857 0         0 $test->record_length($record_length);
858             }
859              
860 2         23 my $size = $self->size();
861 2 50       5 die unless defined $size;
862 2         6 $test->size($size);
863              
864 2         6 1;
865             }
866             catch {
867 1     1   19 0;
868 3         26 };
869              
870 3         88 return $is_valid;
871             }
872              
873             =head2 empty
874              
875             Check if directory item contains information about the actual disk file:
876              
877             my $is_empty = $item->empty();
878              
879             True value will be returned when directory item object is empty.
880              
881             =cut
882              
883             sub empty {
884 2     2 1 23 my ($self) = @_;
885              
886 2         3 my $is_empty = not grep { ord ($_) != 0x00 } @{$self};
  60         89  
  2         5  
887              
888 2         12 return $is_empty;
889             }
890              
891             =head2 writable
892              
893             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:
894              
895             my $is_writable = $item->writable();
896              
897             True value will be returned when directory item object is writable.
898              
899             =cut
900              
901             sub writable {
902 4     4 1 30 my ($self) = @_;
903              
904 4   66     38 my $is_writable = !$self->closed() && $self->type() eq $T_DEL;
905              
906 4         41 return $is_writable;
907             }
908              
909             =head2 clone
910              
911             Clone disk directory item:
912              
913             my $clone = $item->clone();
914              
915             =head2 match_name
916              
917             Check if filename matches given CBM ASCII pattern:
918              
919             my $is_matched = $item->match_name($petscii_pattern);
920              
921             C<$petscii_pattern> is expected to be a CBM ASCII string containing optional wildcard characters. The following wildcards are allowed/recognized:
922              
923             =over
924              
925             =item *
926             An asterisk C<*> character following any program name will yield successful match if filename is starting with that name.
927              
928             =item *
929             A question mark C character used as a wildcard will match any character in a filename.
930              
931             =back
932              
933             =cut
934              
935             sub match_name {
936 29     29 1 16664 my ($self, $petscii_pattern) = @_;
937              
938 29         72 my $name = $self->name(padding_with_a0 => 0);
939              
940 29         87 my @name = split //, $name;
941 29         64 my @pattern = split //, $petscii_pattern;
942              
943 29         72 for (my $i = 0; $i < @pattern; $i++) {
944 135         192 my $match_pattern = ord $pattern[$i];
945 135 100       223 if ($match_pattern == 0x2a) {
946 14         69 return 1;
947             }
948 121         153 my $character = $name[$i];
949 121 100 100     316 unless (defined $character && $match_pattern == 0x3f) {
950 110 100 100     339 if (!defined $character || ord $character != $match_pattern) {
951 4         21 return 0;
952             }
953             }
954             }
955              
956 11 100       23 if (@name == @pattern) {
957 7         36 return 1;
958             }
959              
960 4         19 return 0;
961             }
962              
963             =head2 type_to_string
964              
965             Convert given file type into its three-letter printable ASCII/PETSCII string representation:
966              
967             my $string = D64::Disk::Dir::Item->type_to_string($type, $as_petscii);
968              
969             C defaults to false (meaning that ASCII characters will be returned by default).
970              
971             =cut
972              
973             sub type_to_string {
974 25     25 1 8394 my ($this, $type, $as_petscii) = @_;
975              
976 25 100       110 unless ($as_petscii) {
977 15         43 my @mapping = (
978             'del', # $T_DEL
979             'seq', # $T_SEQ
980             'prg', # $T_PRG
981             'usr', # $T_USR
982             'rel', # $T_REL
983             'cbm', # $T_CBM
984             'dir', # $T_DIR
985             );
986              
987 15 100 66     63 if ($type >= 0 && $type < @mapping) {
988 14         72 return $mapping[$type]
989             }
990             else {
991 1         4 return '???';
992             }
993             }
994             else {
995 10         27 my @mapping = (
996             '44454c', # $T_DEL
997             '534551', # $T_SEQ
998             '505247', # $T_PRG
999             '555352', # $T_USR
1000             '52454c', # $T_REL
1001             '43424d', # $T_CBM
1002             '444952', # $T_DIR
1003             );
1004              
1005 10 100 66     51 if ($type >= 0 && $type < @mapping) {
1006 9         48 return pack 'H*', $mapping[$type];
1007             }
1008             else {
1009 1         4 return pack 'H*', '3f3f3f';
1010             }
1011             }
1012             }
1013              
1014             sub _dump {
1015 23     23   46 my ($self, $value) = @_;
1016              
1017 23         100 my $dump = Data::Dumper->new([$value])->Indent(0)->Terse(1)->Deepcopy(1)->Sortkeys(1)->Dump();
1018              
1019 23         1805 return $dump;
1020             }
1021              
1022             sub is_int {
1023 93     93 0 2196 my ($this, $var) = @_;
1024              
1025 93         396 return _is_int($var);
1026             }
1027              
1028             sub is_str {
1029 36     36 0 2565 my ($this, $var) = @_;
1030              
1031 36         145 return _is_str($var);
1032             }
1033              
1034             sub magic_to_int {
1035 2     2 0 1245 my ($this, $magic) = @_;
1036              
1037 2         30 return _magic_to_int($magic);
1038             }
1039              
1040             sub set_iok {
1041 24     24 0 46 my ($self, $var) = @_;
1042              
1043 24         55 my $var_iok = _set_iok($var);
1044              
1045 24         66 return $var_iok;
1046             }
1047              
1048             =head1 BUGS
1049              
1050             There are no known bugs at the moment. Please report any bugs or feature requests.
1051              
1052             =head1 CAVEATS
1053              
1054             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.
1055              
1056             =head1 EXPORT
1057              
1058             C exports nothing by default.
1059              
1060             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.
1061              
1062             =head1 SEE ALSO
1063              
1064             L, L, L.
1065              
1066             =head1 AUTHOR
1067              
1068             Pawel Krol, Epawelkrol@cpan.orgE.
1069              
1070             =head1 VERSION
1071              
1072             Version 0.08 (2023-05-12)
1073              
1074             =head1 COPYRIGHT AND LICENSE
1075              
1076             Copyright 2013-2023 by Pawel Krol .
1077              
1078             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.
1079              
1080             PLEASE NOTE THAT IT COMES WITHOUT A WARRANTY OF ANY KIND!
1081              
1082             =cut
1083              
1084             1;