File Coverage

blib/lib/D64/Disk/Layout/Dir.pm
Criterion Covered Total %
statement 503 518 97.1
branch 160 178 89.8
condition 90 112 80.3
subroutine 52 52 100.0
pod 16 18 88.8
total 821 878 93.5


line stmt bran cond sub pod time code
1             package D64::Disk::Layout::Dir;
2              
3             =head1 NAME
4              
5             D64::Disk::Layout::Dir - Handling entire Commodore (D64/D71/D81) disk image directories in pure Perl
6              
7             =head1 SYNOPSIS
8              
9             use D64::Disk::Layout::Dir;
10              
11             # Create an empty disk directory instance:
12             my $dir = D64::Disk::Layout::Dir->new();
13              
14             # Create a new disk directory instance providing 18 * 256 bytes of scalar data:
15             my $dir = D64::Disk::Layout::Dir->new(data => $data);
16              
17             # Fetch directory object data as a scalar of 18 * 256 bytes:
18             my $data = $dir->data();
19              
20             # Replace directory providing 18 * 256 bytes of scalar data:
21             $dir->data($data);
22              
23             # Fetch directory data as an array of up to 18 * 8 items:
24             my @items = $dir->items();
25              
26             # Replace directory providing an array of up to 18 * 8 items:
27             $dir->items(@items);
28              
29             # Get count of non-empty items stored in a disk directory:
30             my $num_items = $dir->num_items();
31              
32             # Fetch directory data as an array of 18 * sectors:
33             my @sectors = $dir->sectors();
34              
35             # Replace directory providing an array of 18 * sectors:
36             $dir->sectors(@sectors);
37              
38             # Fetch an item from a directory listing at any given position:
39             my $item = $dir->get(item => $index);
40              
41             # Fetch a list of items from a directory listing matching given PETSCII pattern:
42             my @items = $dir->get(pattern => $petscii_pattern);
43              
44             # Append an item to the end of directory listing, increasing number of files by one element:
45             $dir->push(item => $item);
46              
47             # Pop and return the last directory item, shortening a directory listing by one element:
48             my $item = $dir->pop();
49              
50             # Shift the first directory item, shortening a directory listing by one and moving everything down:
51             my $item = $dir->shift();
52              
53             # Prepend an item to the front of directory listing, and return the new number of elements:
54             my $num_items = $dir->unshift(item => $item);
55              
56             # Mark directory item designated by an offset as deleted:
57             my $num_deleted = $dir->delete(index => $index);
58              
59             # Wipe out directory item designated by an offset completely:
60             my $num_removed = $dir->remove(index => $index);
61              
62             # Add a new directory item to a directory listing:
63             my $is_success = $dir->add(item => $item);
64              
65             # Put an item to a directory listing at any given position:
66             my $is_success = $dir->put(item => $item, index => $index);
67              
68             # Print out formatted disk directory listing:
69             $dir->print();
70              
71             =head1 DESCRIPTION
72              
73             C provides a helper class for C module, enabling users to access and manipulate entire directories of D64/D71/D81 disk images in an object oriented way without the hassle of worrying about the meaning of individual bits and bytes describing each sector data on a disk directory track. 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.
74              
75             =head1 METHODS
76              
77             =cut
78              
79 15     15   1962761 use bytes;
  15         136  
  15         714  
80 15     15   486 use strict;
  15         30  
  15         308  
81 15     15   72 use utf8;
  15         24  
  15         126  
82 15     15   368 use warnings;
  15         28  
  15         977  
83              
84             our $VERSION = '0.06';
85              
86 15     15   129 use D64::Disk::Dir::Item qw(:types);
  15         42  
  15         3158  
87 15     15   123 use D64::Disk::Layout::Sector;
  15         48  
  15         414  
88 15     15   81 use Data::Dumper;
  15         29  
  15         910  
89 15     15   9401 use List::MoreUtils qw(uniq);
  15         203975  
  15         95  
90 15     15   17123 use Readonly;
  15         40  
  15         874  
91 15     15   101 use Text::Convert::PETSCII qw(:convert :validate);
  15         28  
  15         11577  
92              
93             require XSLoader;
94             XSLoader::load(__PACKAGE__, $VERSION);
95              
96             Readonly our $ITEMS_PER_SECTOR => 8;
97             Readonly our $TOTAL_SECTOR_COUNT => 18;
98              
99             Readonly our $ITEM_SIZE => $D64::Disk::Dir::Item::ITEM_SIZE;
100             Readonly our $SECTOR_DATA_SIZE => $D64::Disk::Layout::Sector::SECTOR_DATA_SIZE;
101              
102             # First directory track and sector:
103             Readonly our $DIRECTORY_FIRST_TRACK => 0x12;
104             Readonly our $DIRECTORY_FIRST_SECTOR => 0x01;
105              
106             Readonly our @TRACK_WRITE_ORDER => (
107             0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
108             0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
109             0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
110             );
111             Readonly our @SECTOR_WRITE_ORDER => (
112             0x01, 0x04, 0x07, 0x0a, 0x0d, 0x10,
113             0x02, 0x05, 0x08, 0x0b, 0x0e, 0x11,
114             0x03, 0x06, 0x09, 0x0c, 0x0f, 0x12,
115             );
116              
117             Readonly our $MAX_ENTRIES => $TOTAL_SECTOR_COUNT * $ITEMS_PER_SECTOR;
118              
119             =head2 new
120              
121             Create an empty disk directory instance:
122              
123             my $dir = D64::Disk::Layout::Dir->new();
124              
125             Create a new disk directory instance providing 18 * 256 bytes of scalar data:
126              
127             my $dir = D64::Disk::Layout::Dir->new(data => $data);
128              
129             Create a new disk directory instance given array with 18 * 256 bytes of data:
130              
131             my $dir = D64::Disk::Layout::Dir->new(data => \@data);
132              
133             Alternatively setup source data structure required to initialize new object using 18 * sector objects:
134              
135             my @sectors = (
136             # It needs to be a list of D64::Disk::Layout::Sector objects:
137             D64::Disk::Layout::Sector->new(data => $sector1, track => 18, sector => 1),
138             D64::Disk::Layout::Sector->new(data => $sector2, track => 18, sector => 4),
139             D64::Disk::Layout::Sector->new(data => $sector3, track => 18, sector => 7),
140             # It needs to contain as many sectors as large directory may be:
141             ...
142             );
143              
144             Create a new disk directory instance providing source sector data:
145              
146             my $dir = D64::Disk::Layout::Dir->new(sectors => \@sectors);
147              
148             Directory object may also be initialized using the list of directory item objects:
149              
150             my @items = (
151             # It needs to be a list of D64::Disk::Dir::Item objects:
152             D64::Disk::Dir::Item->new($item1),
153             D64::Disk::Dir::Item->new($item2),
154             D64::Disk::Dir::Item->new($item3),
155             # Up to the maximum number of directory entries (18 * 8 = 144):
156             ...
157             );
158              
159             Create a new disk directory instance providing list of dir items:
160              
161             my $dir = D64::Disk::Layout::Dir->new(items => \@items);
162              
163             Individual directory items are stored, accessed and manipulated as C objects.
164              
165             =cut
166              
167             sub new {
168 258     258 1 2774581 my ($this) = CORE::shift;
169 258   33     1612 my $class = ref ($this) || $this;
170 258         891 my $object = $class->_init();
171 258         715 my $self = bless $object, $class;
172 258         1171 $self->_setup(@_);
173 242         1024 return $self;
174             }
175              
176             sub _init {
177 455     455   1077 my ($class) = @_;
178              
179 455         2273 my @items = map { D64::Disk::Dir::Item->new() } (1 .. $ITEMS_PER_SECTOR * $TOTAL_SECTOR_COUNT);
  65520         3663404  
180              
181 455         41268 my $object = {
182             items => \@items,
183             sector_order => [@SECTOR_WRITE_ORDER],
184             track_order => [@TRACK_WRITE_ORDER],
185             };
186              
187 455         67990 return $object;
188             }
189              
190             sub _setup {
191 258     258   930 my ($self, %args) = @_;
192              
193 258 100       1064 $self->data($args{data}) if exists $args{data};
194 247 100       1218 $self->items($args{items}) if exists $args{items};
195 246 100       854 $self->sectors($args{sectors}) if exists $args{sectors};
196              
197 242         627 return undef;
198             }
199              
200             sub _validate_data {
201 55     55   176 my ($self, $data) = @_;
202              
203 55         230 my $expected_data_size = $TOTAL_SECTOR_COUNT * $SECTOR_DATA_SIZE;
204              
205 55 100       641 unless (defined $data) {
206 1         212 die sprintf q{Unable to initialize disk directory: Undefined value of data (expected %d bytes)}, $expected_data_size;
207             }
208              
209             # Convert scalar data into an array:
210 54 100 66     363 unless (ref $data) {
211 15     15   127 no bytes;
  15         41  
  15         165  
212 14         14601 $data = [ split //, $data ];
213             }
214             elsif (ref $data ne 'ARRAY') {
215             die sprintf q{Unable to initialize disk directory: Invalid arguments given (expected %d bytes)}, $expected_data_size;
216             }
217              
218 53 100       2752 unless (scalar (@{$data}) == $expected_data_size) {
  53         254  
219 6         11 die sprintf q{Unable to initialize disk directory: Invalid amount of data (got %d bytes, but required %d)}, scalar (@{$data}), $expected_data_size;
  6         1213  
220             }
221              
222 47         170 for (my $i = 0; $i < @{$data}; $i++) {
  202823         371685  
223 202779         289044 my $byte_value = $data->[$i];
224 202779 100       324509 if (ref $byte_value) {
225 1         221 die sprintf q{Unable to initialize disk directory: Invalid data type at offset %d (%s)}, $i, ref $byte_value;
226             }
227 202778 100       313651 unless ($self->_is_valid_byte_value($byte_value)) {
228 2         9 die sprintf q{Unable to initialize disk directory: Invalid byte value at offset %d (%s)}, $i, $self->_dump($byte_value);
229             }
230             }
231              
232 44         160 return @{$data};
  44         42162  
233             }
234              
235             sub _validate_sectors {
236 60     60   183 my ($self, $sectors) = @_;
237              
238 60         238 my $expected_sectors_size = $TOTAL_SECTOR_COUNT;
239              
240 60 100       315 unless (scalar (@{$sectors}) == $expected_sectors_size) {
  60         326  
241 3         8 die sprintf q{Unable to initialize disk directory: Invalid number of sectors (got %d sectors, but required %d)}, scalar (@{$sectors}), $expected_sectors_size;
  3         701  
242             }
243              
244             # Remove duplicate sectors (objects sharing the same track/sector position):
245 57         231 my $count_removed = $self->_remove_duplicate_sectors($sectors);
246              
247 57 50       194 unless (defined $sectors) {
248 0         0 die sprintf q{Unable to initialize disk directory: Undefined value of sectors (expected %d sectors)}, $expected_sectors_size;
249             }
250              
251 57 50       227 unless (ref $sectors eq 'ARRAY') {
252 0         0 die sprintf q{Unable to initialize disk directory: Invalid arguments given (expected %d sectors)}, $expected_sectors_size;
253             }
254              
255 57 100       99 unless (scalar (@{$sectors}) == $expected_sectors_size) {
  57         207  
256 1         4 die sprintf q{Unable to initialize disk directory: Invalid number of sectors (got %d sectors, but required %d)}, scalar (@{$sectors}), $expected_sectors_size;
  1         238  
257             }
258              
259 56         149 for (my $i = 0; $i < @{$sectors}; $i++) {
  1064         1915  
260 1008         1359 my $sector_value = $sectors->[$i];
261 1008 50       2425 unless ($sector_value->isa('D64::Disk::Layout::Sector')) {
262 0         0 die sprintf q{Unable to initialize disk directory: Invalid sector type at offset %d (%s)}, $i, ref $sector_value;
263             }
264             }
265              
266 56         210 return $sectors;
267             }
268              
269             sub _remove_duplicate_sectors {
270 57     57   169 my ($self, $sectors) = @_;
271              
272 57         175 my $count_removed = 0;
273              
274 57         175 for (my $i = 0; $i < @{$sectors}; $i++) {
  1082         2013  
275 1025         1488 my $sector_object = $sectors->[$i];
276 1025         1951 my $track = $sector_object->track();
277 1025         11563 my $sector = $sector_object->sector();
278 1025         11009 for (my $j = $i + 1; $j < @{$sectors}; $j++) {
  9741         196541  
279 8716         11681 my $test_sector = $sectors->[$j];
280 8716 100 66     15825 if ($test_sector->track() == $track && $test_sector->sector() == $sector) {
281 1         25 splice @{$sectors}, $j, 1;
  1         3  
282 1         2 $j--;
283 1         19 $count_removed++;
284             }
285             }
286             }
287              
288 57         208 return $count_removed;
289             }
290              
291             sub _find_sector {
292 1080     1080   2493 my ($self, $sectors, $track, $sector) = @_;
293              
294 1080 50 33     3673 return unless defined $track && defined $sector;
295              
296 1080         1608 for my $sector_object (@{$sectors}) {
  1080         2490  
297 9664 100 66     200047 if ($sector_object->track() == $track && $sector_object->sector() == $sector) {
298 1080         26795 return $sector_object;
299             }
300             }
301              
302 0         0 return undef;
303             }
304              
305             sub _validate_items {
306 142     142   338 my ($self, $items) = @_;
307              
308 142         530 my $expected_items_size = $ITEMS_PER_SECTOR * $TOTAL_SECTOR_COUNT;
309              
310 142 50       1308 unless (defined $items) {
311 0         0 die sprintf q{Unable to initialize disk directory: Undefined value of items (expected up to %d items)}, $expected_items_size;
312             }
313              
314 142 50       479 unless (ref $items eq 'ARRAY') {
315 0         0 die sprintf q{Unable to initialize disk directory: Invalid arguments given (expected up to %d items)}, $expected_items_size;
316             }
317              
318 142 100       247 unless (scalar (@{$items}) <= $expected_items_size) {
  142         490  
319 1         3 die sprintf q{Unable to initialize disk directory: Invalid number of items (got %d items, but required up to %d)}, scalar (@{$items}), $expected_items_size;
  1         231  
320             }
321              
322 141         388 for (my $i = 0; $i < @{$items}; $i++) {
  1812         1093032  
323 1671         3030 my $item_value = $items->[$i];
324 1671 50       5752 unless ($item_value->isa('D64::Disk::Dir::Item')) {
325 0         0 die sprintf q{Unable to initialize disk directory: Invalid item type at offset %d (%s)}, $i, ref $item_value;
326             }
327 1671 50       3920 unless ($item_value->validate()) {
328 0         0 die sprintf q{Unable to initialize disk directory: Invalid item value at offset %d (%s)}, $i, $self->_dump($item_value);
329             }
330             }
331              
332 141         377 return undef;
333             }
334              
335             =head2 data
336              
337             Fetch directory object data as a scalar of 18 * 256 bytes:
338              
339             my $data = $dir->data();
340              
341             Fetch directory object data as an array of 18 * 256 bytes:
342              
343             my @data = $dir->data();
344              
345             Replace directory providing 18 * 256 bytes of scalar data:
346              
347             $dir->data($data);
348              
349             Replace directory given array with 18 * 256 bytes of data:
350              
351             $dir->data(@data);
352             $dir->data(\@data);
353              
354             =cut
355              
356             sub data {
357 155     155 1 530820 my ($self, @args) = @_;
358              
359 155 100       609 if (@args) {
360 55         186 my ($arg) = @args;
361 55 100       227 my $data = (scalar @args == 1) ? $arg : \@args;
362 55         270 my @data = $self->_validate_data($data);
363              
364 44         989 my $iter = $self->_get_order_from_data(\@data);
365 44         276 my ($track_order, $sector_order) = $self->_get_order($iter);
366              
367             ## TODO: Optimize code below by constructing directory "items" directly here!!!
368              
369             # Convert data into sectors and initialize object:
370 44         111 my @sectors;
371 44         191 while (my @sector_data = splice @data, 0, $SECTOR_DATA_SIZE) {
372 792         8745 my $track = CORE::shift @{$track_order};
  792         1534  
373 792         1265 my $sector = CORE::shift @{$sector_order};
  792         1294  
374 792         2789 my $sector_object = D64::Disk::Layout::Sector->new(data => \@sector_data, track => $track, sector => $sector);
375 792         2313134 CORE::push @sectors, $sector_object;
376             }
377 44         587 $self->sectors(@sectors);
378             }
379              
380 144         509 my $items = $self->{items};
381 144         551 my $num_items = $self->num_items();
382              
383             # Get directory object data as an array of bytes:
384 144         7202 my @data;
385 144         414 for (my $i = 0; $i < @{$items}; $i++) {
  20880         41578  
386 20736         43936 my @item_data = $items->[$i]->data();
387 20736 100 100     276767 if ($i % $ITEMS_PER_SECTOR == 0 && ($i + $ITEMS_PER_SECTOR) < $num_items) {
    100 66        
    100 100        
    100 66        
388             # Add information about the next directory track/sector data:
389 21         359 CORE::push @data, chr $self->{track_order}->[$i / $ITEMS_PER_SECTOR + 1];
390 21         237 CORE::push @data, chr $self->{sector_order}->[$i / $ITEMS_PER_SECTOR + 1];
391             }
392             elsif ($i % $ITEMS_PER_SECTOR == 0 && ($i + $ITEMS_PER_SECTOR) >= $num_items && $i < $num_items) {
393 118         3050 CORE::push @data, chr (0x00), chr (0xff);
394             }
395             elsif ($i == 0 && $num_items == 0) {
396 26         910 CORE::push @data, chr (0x00), chr (0xff);
397             }
398             elsif ($i % $ITEMS_PER_SECTOR == 0) {
399 2427         53417 CORE::push @data, chr (0x00), chr (0xff);
400             }
401             else {
402 18144         236380 CORE::push @data, chr (0x00), chr (0x00);
403             }
404 20736         152670 CORE::push @data, @item_data;
405             }
406              
407 144 100       67293 return wantarray ? @data : join '', @data;
408             }
409              
410             sub _get_order_from_data {
411 44     44   155 my ($self, $data) = @_;
412              
413 44         112 my $i = 0;
414              
415             return sub {
416 56     56   234 my $index = $SECTOR_DATA_SIZE * $i++;
417              
418 56         410 my $track = ord $data->[$index + 0];
419 56         180 my $sector = ord $data->[$index + 1];
420              
421 56         181 return ($track, $sector);
422 44         711 };
423             }
424              
425             sub _get_order {
426 100     100   317 my ($self, $next) = @_;
427              
428 100         770 my @track_order = @TRACK_WRITE_ORDER;
429 100         8153 my @sector_order = @SECTOR_WRITE_ORDER;
430              
431 100         7724 $sector_order[0] = _magic_to_int($DIRECTORY_FIRST_SECTOR);
432              
433 100         1057 for (my $i = 0; $i < @sector_order; $i++) {
434 128         348 my ($track, $sector) = $next->();
435              
436 128 100       583 last if $track == 0x00;
437              
438 28         169 splice @track_order, $i + 1, 0, $track;
439 28         120 splice @sector_order, $i + 1, 0, $sector;
440             }
441              
442             # Remove duplicated track/sector order pairs:
443 100         460 for (my $i = 0; $i < @sector_order; $i++) {
444 1800         2533 my $track = $track_order[$i];
445 1800         2407 my $sector = $sector_order[$i];
446 1800         3401 for (my $j = $i + 1; $j < @sector_order; $j++) {
447 15356 100 66     46787 if ($track_order[$j] == $track && $sector_order[$j] == $sector) {
448 28         70 splice @track_order, $j, 1;
449 28         67 splice @sector_order, $j, 1;
450 28         74 $j--;
451             }
452             }
453             }
454              
455 100         446 return (\@track_order, \@sector_order);
456             }
457              
458             =head2 items
459              
460             Fetch directory object data as an array of up to 18 * 8 items:
461              
462             my @items = $dir->items();
463              
464             This method returns only non-empty directory items.
465              
466             Replace entire directory providing an array of up to 18 * 8 items:
467              
468             $dir->items(@items);
469             $dir->items(\@items);
470              
471             An entire directory object data will be replaced when calling this method. This will happen even when number of items provided as an input parameter is less than the number of non-empty items stored in an object before method was invoked.
472              
473             =cut
474              
475             sub items {
476 163     163 1 82049 my ($self, @args) = @_;
477              
478 163 100       474 if (@args) {
479 142         308 my ($arg) = @args;
480 142 50       671 my $items = (scalar @args == 1) ? (ref $arg ? $arg : [ $arg ]) : \@args;
    100          
481 142         607 $self->_validate_items($items);
482              
483 141         487 my $object = $self->_init();
484 141         30515 $self->{items} = $object->{items};
485 141         623 $self->{sector_order} = $object->{sector_order};
486 141         421 $self->{track_order} = $object->{track_order};
487              
488 141         281 my $i = 0;
489              
490 141         279 for my $item (@{$items}) {
  141         456  
491 1671         47911 $self->{items}->[$i] = $item->clone();
492 1671         3347 $i++;
493             }
494             }
495              
496 162         391 my $items = $self->{items};
497 162         550 my $num_items = $self->num_items();
498              
499 162         7663 my @items;
500              
501 162         505 for (my $i = 0; $i < $num_items; $i++) {
502 765         23086 CORE::push @items, $items->[$i]->clone();
503             }
504              
505 162         1433 return @items;
506             }
507              
508             =head2 num_items
509              
510             Get count of non-empty items stored in a disk directory:
511              
512             my $num_items = $dir->num_items();
513              
514             =cut
515              
516             sub num_items {
517 645     645 1 1791 my ($self, @args) = @_;
518              
519 645         1312 my $items = $self->{items};
520              
521 645         1372 for (my $i = 0; $i < @{$items}; $i++) {
  2854         108729  
522 2854         4436 my $item = $items->[$i];
523              
524 2854 100       6453 return $i if $item->empty();
525             }
526              
527 0         0 return scalar @{$items};
  0         0  
528             }
529              
530             sub _last_item_index {
531 49     49   105 my ($self) = @_;
532              
533 49         103 my $num_items = $self->num_items();
534              
535 49         2322 return $num_items - 1; # -1 .. ($ITEMS_PER_SECTOR * $TOTAL_SECTOR_COUNT - 1)
536             }
537              
538             =head2 sectors
539              
540             Fetch directory object data as an array of 18 * sector objects:
541              
542             my @sectors = $dir->sectors();
543              
544             Replace entire directory providing an array of 18 * sector objects:
545              
546             $dir->sectors(@sectors);
547             $dir->sectors(\@sectors);
548              
549             =cut
550              
551             sub sectors {
552 72     72 1 92346 my ($self, @args) = @_;
553              
554 72 100       289 if (@args) {
555 60         158 my ($arg) = @args;
556 60 50       281 my $sectors = (scalar @args == 1) ? (ref $arg ? $arg : [ $arg ]) : \@args;
    100          
557 60         332 $sectors = $self->_validate_sectors($sectors);
558              
559 56         286 my $object = $self->_init();
560 56         20424 $self->{items} = $object->{items};
561              
562 56         421 my $iter = $self->_get_order_from_sectors($sectors);
563 56         333 my ($track_order, $sector_order) = $self->_get_order($iter);
564              
565 56         299 $self->{sector_order} = $sector_order;
566 56         212 $self->{track_order} = $track_order;
567              
568 56         132 my $sector = $sector_order->[0];
569 56         124 my $track = $track_order->[0];
570              
571 56         138 my $index = 0;
572 56         193 while (my $sector_object = $self->_find_sector($sectors, $track, $sector)) {
573 1008         2348 my @items = $self->_sector_to_items($sector_object);
574              
575 1008         1769 splice @{$self->{items}}, $index * $ITEMS_PER_SECTOR, $ITEMS_PER_SECTOR, @items;
  1008         3069  
576              
577 1008         24666 $index++;
578              
579 1008         2082 $sector = $sector_order->[$index];
580 1008         1727 $track = $track_order->[$index];
581              
582 1008 100 66     6677 last unless defined $track && defined $sector;
583             }
584             }
585              
586 68         263 my $items = $self->{items};
587 68         318 my $num_items = $self->num_items();
588              
589             # Get directory object data as an array of sectors:
590 68         3546 my @sectors;
591 68         314 for (my $i = 0; $i < $TOTAL_SECTOR_COUNT; $i++) {
592 1224         8189 my $track = $self->{track_order}->[$i];
593 1224         2533 my $sector = $self->{sector_order}->[$i];
594              
595 1224         1765 my @data;
596 1224         3041 for (my $j = 0; $j < $ITEMS_PER_SECTOR; $j++) {
597 9792         51068 my @item_data = $items->[$i * $ITEMS_PER_SECTOR + $j]->data();
598 9792 100 100     197573 if ($j == 0 && ($i + 1) * $ITEMS_PER_SECTOR < $num_items) {
    100 66        
    100 100        
    100 100        
      66        
599             # Add information about the next directory track/sector data:
600 38         383 CORE::push @data, chr $self->{track_order}->[$i + 1];
601 38         149 CORE::push @data, chr $self->{sector_order}->[$i + 1];
602             }
603             elsif ($j == 0 && ($i + 1) * $ITEMS_PER_SECTOR >= $num_items && $i * $ITEMS_PER_SECTOR < $num_items) {
604 57         1235 CORE::push @data, chr (0x00), chr (0xff);
605             }
606             elsif ($i == 0 && $j == 0 && $num_items == 0) {
607 11         301 CORE::push @data, chr (0x00), chr (0xff);
608             }
609             elsif ($j == 0) {
610 1118         20956 CORE::push @data, chr (0x00), chr (0xff);
611             }
612             else {
613 8568         15452 CORE::push @data, chr (0x00), chr (0x00);
614             }
615 9792         70992 CORE::push @data, @item_data;
616             }
617              
618 1224         8629 my $sector_object = D64::Disk::Layout::Sector->new(data => \@data, track => $track, sector => $sector);
619 1224         3598969 CORE::push @sectors, $sector_object;
620             }
621              
622 68         30764 return @sectors;
623             }
624              
625             =head2 num_sectors
626              
627             Get total number of allocated sectors that can be used to store disk directory data:
628              
629             my $num_sectors = $dir->num_sectors(count => 'all');
630              
631             In the case of a C disk image format, the value of C<18> is always returned, as this is a standard number of sectors designated to store disk directory data.
632              
633             Get number of currently used sectors that are used to store actual disk directory data:
634              
635             my $num_sectors = $dir->num_sectors(count => 'used');
636              
637             In this case method call returns an integer value between C<0> and C<18> (total count of sectors used to store actual data), i.a. for an empty disk directory C<0> is returned, and for a disk directory filled with more than 136 files the value of C<18> will be retrieved.
638              
639             C parameter defaults to C.
640              
641             =cut
642              
643             sub num_sectors {
644 5     5 1 50 my ($self, %args) = @_;
645              
646 5   50     22 my $mode = $args{'count'} || 'all';
647              
648 5 100       29 if ($mode eq 'all') {
    50          
649 2         10 return $TOTAL_SECTOR_COUNT;
650             }
651             elsif ($mode eq 'used') {
652 3         15 my $last_item_index = $self->_last_item_index();
653              
654 3         20 while (++$last_item_index % 8) {};
655              
656 3         20 return int ($last_item_index / 8);
657             }
658             else {
659 0         0 die sprintf q{Invalid value of "count" parameter: %s}, $mode;
660             }
661             }
662              
663             sub _get_order_from_sectors {
664 56     56   284 my ($self, $sectors) = @_;
665              
666 56         299 my $track = $DIRECTORY_FIRST_TRACK;
667 56         425 my $sector = $DIRECTORY_FIRST_SECTOR;
668              
669             return sub {
670 72     72   310 my $sector_object = $self->_find_sector($sectors, $track, $sector);
671 72 50       236 return unless $sector_object;
672              
673 72         258 my $sector_data = $sector_object->data();
674              
675 72         4090 $track = ord substr $sector_data, 0, 1;
676 72         172 $sector = ord substr $sector_data, 1, 1;
677              
678 72         235 return ($track, $sector);
679 56         1053 };
680             }
681              
682             sub _sector_to_items {
683 1008     1008   1787 my ($self, $sector_object) = @_;
684              
685 1008         2341 my @data = $sector_object->data();
686              
687 1008         73349 my @items;
688              
689 1008         2879 for (my $i = 0; $i < $ITEMS_PER_SECTOR; $i++) {
690 8064         3161854 my $index = 2 + $i * ($ITEM_SIZE + 2);
691 8064         40444 my @item_data = @data[$index .. $index + $ITEM_SIZE - 1];
692 8064         83105 CORE::push @items, D64::Disk::Dir::Item->new(@item_data);
693             }
694              
695 1008         464850 return @items;
696             }
697              
698             =head2 get
699              
700             Fetch an item from a directory listing at any given position:
701              
702             my $item = $dir->get(index => $index);
703              
704             C<$index> indicates an offset from the beginning of a directory listing, with count starting from C<0>. When C<$index> indicates an element beyond the number of non-empty items stored in a disk directory, an undefined value will be returned.
705              
706             Fetch a list of items from a directory listing matching given PETSCII pattern:
707              
708             use Text::Convert::PETSCII qw(:convert);
709              
710             my $pattern = ascii_to_petscii 'workstage*';
711              
712             my @items = $dir->get(pattern => $pattern);
713              
714             C is expected to be any valid PETSCII text string. Such call to this method always returns B items with filename matching given PETSCII pattern.
715              
716             =cut
717              
718             sub get {
719 17     17 1 767 my ($self, %args) = @_;
720              
721 17 50 66     81 if (exists $args{index} && exists $args{pattern}) {
722 0         0 die q{Unable to fetch an item from a directory listing: ambiguous file index/matching pattern specified (you cannot specify both parameters at the same time)};
723             }
724              
725 17 50 66     70 unless (exists $args{index} || exists $args{pattern}) {
726 0         0 die q{Unable to fetch an item from a directory listing: Missing index/pattern parameter (which element did you want to get?)};
727             }
728              
729 17         32 my $index = $args{index};
730 17         34 my $pattern = $args{pattern};
731              
732 17 100       40 if (exists $args{index}) {
733              
734 11         43 $self->_validate_index($index, 'get');
735              
736 6         18 my $num_items = $self->num_items();
737 6         309 my $items = $self->{items};
738              
739 6 100       22 if ($index < $num_items) {
740 4         34 return $items->[$index];
741             }
742             else {
743 2         15 return undef;
744             }
745             }
746             else {
747              
748 6         20 $self->_validate_pattern($pattern, 'get');
749              
750 6         15 my @items = $self->items();
751              
752 6         14 for my $item (@items) {
753 18         50 my $is_matched = $item->match_name($pattern);
754              
755 18 100       2197 $item = undef unless $is_matched;
756             }
757              
758 6         13 return grep { defined } @items;
  18         51  
759             }
760             }
761              
762             sub _validate_index {
763 63     63   156 my ($self, $index, $operation) = @_;
764              
765 63         149 my $items = $self->{items};
766 63         101 my $maximum_allowed_position = scalar (@{$items}) - 1;
  63         129  
767              
768 63 100 100     217 if (D64::Disk::Dir::Item->is_int($index) && $index >= 0x00 && $index <= $maximum_allowed_position) {
      100        
769 39         458 return undef;
770             }
771              
772 24 100       294 my $dumped_index = $self->_is_valid_number_value($index) ? $index : $self->_dump($index);
773              
774 24         158 my %description = (
775             'add' => 'Unable to add an item to a directory listing',
776             'delete' => 'Unable to mark disk directory item as deleted',
777             'get' => 'Unable to fetch an item from a directory listing',
778             'put' => 'Unable to put an item to a directory listing',
779             'remove' => 'Unable to entirely remove directory item',
780             );
781              
782 24         380 die sprintf q{%s: Invalid index parameter (got "%s", but expected an integer between 0 and %d)}, $description{$operation}, $dumped_index, $maximum_allowed_position;
783             }
784              
785             sub _validate_pattern {
786 56     56   168 my ($self, $pattern, $operation) = @_;
787              
788 56 100 100     345 if (defined ($pattern) && !ref ($pattern) && is_valid_petscii_string($pattern) && length ($pattern) > 0 && length ($pattern) <= 16) {
      100        
      100        
      100        
789 46         893 return undef;
790             }
791              
792 10 100       159 my $pattern_to_dump = ref ($pattern) ? $pattern :
    100          
793             is_printable_petscii_string($pattern) ? petscii_to_ascii($pattern) :
794             $pattern;
795              
796 10 50       801 my $dumped_pattern = !defined ($pattern) ? 'undef' :
    100          
797             $self->_is_valid_number_value($pattern) ? $pattern :
798             $self->_dump($pattern_to_dump);
799              
800 10         46 $dumped_pattern =~ s/^"(.*)"$/$1/;
801 10         44 $dumped_pattern =~ s/^'(.*)'$/$1/;
802              
803 10         44 my %description = (
804             'delete' => 'Unable to mark disk directory item as deleted',
805             'get' => 'Unable to fetch an item from a directory listing',
806             'remove' => 'Unable to entirely remove directory item',
807             );
808              
809 10         144 die sprintf q{%s: Invalid pattern parameter (got "%s", but expected a valid PETSCII text string)}, $description{$operation}, $dumped_pattern;
810             }
811              
812             sub _validate_item_object {
813 60     60   174 my ($self, $item, $operation) = @_;
814              
815 60         326 my %description = (
816             'add' => 'Unable to add an item to a directory listing',
817             'prepended' => 'Failed to validate prepended directory item',
818             'pushed' => 'Failed to validate pushed directory item',
819             'put' => 'Unable to put an item to a directory listing',
820             );
821              
822 60 100       173 unless (defined $item) {
823 2         32 die sprintf q{%s: Undefined item parameter (expected valid item object)}, $description{$operation};
824             }
825              
826 58 100 66     507 unless (ref $item && $item->isa('D64::Disk::Dir::Item')) {
827 4         82 die sprintf q{%s: Invalid item parameter (got "%s", but expected a valid item object)}, $description{$operation}, ref $item;
828             }
829              
830 50         157 return undef;
831             }
832              
833             =head2 push
834              
835             Append an item to the end of directory listing, increasing number of files by one element:
836              
837             $dir->push(item => $item);
838              
839             C<$item> is expected to be a valid C object. This method will not work when number of non-empty items stored in a disk directory has already reached its maximum.
840              
841             =cut
842              
843             sub push {
844 10     10 1 4355 my ($self, %args) = @_;
845              
846 10         30 my $num_items = $self->num_items();
847 10 100       475 if ($num_items >= $MAX_ENTRIES) {
848 1         11 warn sprintf q{Unable to push another item to a directory listing, maximum number of %d entries has been reached}, $MAX_ENTRIES;
849             }
850              
851 10         124 my $item = $args{item};
852 10         34 $self->_validate_item_object($item, 'pushed');
853              
854 7         30 my $last_item_index = $self->_last_item_index();
855              
856 7         289 $self->{items}->[$last_item_index + 1] = $item->clone();
857              
858 7         32 $num_items = $self->num_items();
859              
860 7         298 return $num_items;
861             }
862              
863             =head2 pop
864              
865             Pop and return the last non-empty directory item, shortening a directory listing by one element:
866              
867             my $item = $dir->pop();
868              
869             When there is at least one non-empty item stored in a disk directory, a C object will be returned. Otherwise return value is undefined.
870              
871             =cut
872              
873             sub pop {
874 18     18 1 97 my ($self, %args) = @_;
875              
876 18         36 my $last_item_index = $self->_last_item_index();
877              
878 18 100       47 return if $last_item_index < 0;
879              
880 14         23 my $item = $self->{items}->[$last_item_index];
881 14         36 $self->{items}->[$last_item_index] = D64::Disk::Dir::Item->new();
882              
883 14         1110 return $item->clone();
884             }
885              
886             =head2 shift
887              
888             Shift the first directory item, shortening a directory listing by one and moving everything down:
889              
890             my $item = $dir->shift();
891              
892             When there is at least one non-empty item stored in a disk directory, a C object will be returned. Otherwise return value is undefined.
893              
894             =cut
895              
896             sub shift {
897 21     21 1 148 my ($self, %args) = @_;
898              
899 21         58 my $last_item_index = $self->_last_item_index();
900              
901 21 100       94 return if $last_item_index < 0;
902              
903 17         40 my $items = $self->{items};
904              
905 17         26 my $item = CORE::shift @{$items};
  17         31  
906 17         31 CORE::push @{$items}, D64::Disk::Dir::Item->new();
  17         48  
907              
908 17         2246 return $item->clone();
909             }
910              
911             =head2 unshift
912              
913             Prepend an item to the front of directory listing, and return the new number of elements:
914              
915             my $num_items = $dir->unshift(item => $item);
916              
917             C<$item> is expected to be a valid C object. This method will not work when number of non-empty items stored in a disk directory has already reached its maximum.
918              
919             =cut
920              
921             sub unshift {
922 13     13 1 5894 my ($self, %args) = @_;
923              
924 13         48 my $num_items = $self->num_items();
925 13 100       712 if ($num_items >= $MAX_ENTRIES) {
926 1         14 warn sprintf q{Unable to prepend an item to the front of directory listing, maximum number of %d entries has been reached}, $MAX_ENTRIES;
927             }
928              
929 13         178 my $item = $args{item};
930 13         69 $self->_validate_item_object($item, 'prepended');
931              
932 10         28 my $items = $self->{items};
933 10         23 CORE::pop @{$items};
  10         20  
934 10         48 CORE::unshift @{$items}, $item->clone();
  10         538  
935              
936 10         40 $num_items = $self->num_items();
937              
938 10         442 return $num_items;
939             }
940              
941             =head2 delete
942              
943             Mark directory item designated by an offset as deleted:
944              
945             my $num_deleted = $dir->delete(index => $index);
946              
947             Mark directory item being the first one to match given PETSCII pattern as deleted:
948              
949             use Text::Convert::PETSCII qw(:convert);
950              
951             my $pattern = ascii_to_petscii 'workstage*';
952              
953             my $num_deleted = $dir->delete(pattern => $pattern, global => 0);
954              
955             Mark all directory items matching given PETSCII pattern as deleted:
956              
957             use Text::Convert::PETSCII qw(:convert);
958              
959             my $pattern = ascii_to_petscii 'workstage*';
960              
961             my $num_deleted = $dir->delete(pattern => $pattern, global => 1);
962              
963             C is expected to be any valid PETSCII text string. C parameter defaults to C<0>, hence deleting only a single file matching given criteria by default. When set to any C value, it will trigger deletion of B items with filename matching given PETSCII pattern.
964              
965             A call to this method always returns the number of successfully deleted items. When deleting an item designated by an offset of an already deleted directory item, such operation does not contribute to the count of successfully deleted items during such a particular method call. In other words, delete an item once, and you get it counted as a successfully deleted one, delete the same item again, and it will not be counted as a deleted one anymore. Of course an item remains delete in a directory listing, it just does not contribute to a value that is returned from this method's call.
966              
967             Note that this method does not remove an entry from directory layout, it only marks it as deleted. In order to wipe out an entry entirely, see description of L method.
968              
969             =cut
970              
971             sub delete {
972 35     35 1 2381 my ($self, %args) = @_;
973              
974 35 100 100     130 if (exists $args{index} && exists $args{pattern}) {
975 1         12 die q{Unable to mark directory item as deleted: ambiguous deletion index/pattern specified (you cannot specify both parameters at the same time)};
976             }
977              
978 34 100 100     141 unless (exists $args{index} || exists $args{pattern}) {
979 1         14 die q{Unable to mark directory item as deleted: Missing index/pattern parameter (which element did you want to delete?)};
980             }
981              
982 33         62 my $index = $args{index};
983 33         65 my $global = $args{global};
984 33         55 my $pattern = $args{pattern};
985              
986 33         75 my $num_items = $self->num_items();
987 33         1502 my $items = $self->{items};
988              
989 33 100       77 if (exists $args{index}) {
990              
991 10         39 $self->_validate_index($index, 'delete');
992              
993 5 100       17 if ($index < $num_items) {
994 4         8 my $item = $items->[$index];
995 4         15 my $count = $self->_delete_item($item);
996 4         14 return $count;
997             }
998             else {
999 1         5 return 0;
1000             }
1001             }
1002             else {
1003              
1004 23         125 $self->_validate_pattern($pattern, 'delete');
1005              
1006 18         29 my $num_deleted = 0;
1007              
1008 18         56 for (my $i = 0; $i < $num_items; $i++) {
1009              
1010 35         709 my $item = $items->[$i];
1011              
1012 35 100       92 if ($item->match_name($pattern)) {
1013              
1014 25         3099 my $count = $self->_delete_item($item);
1015              
1016 25         40 $num_deleted += $count;
1017              
1018             # File got deleted and only one was requested to get deleted:
1019 25 100 100     117 last if $count and !$global;
1020             }
1021             }
1022              
1023 18         503 return $num_deleted;
1024             }
1025             }
1026              
1027             sub _delete_item {
1028 29     29   62 my ($self, $item) = @_;
1029              
1030 29         69 my $was_closed = $item->closed();
1031 29         457 my $was_deleted = $item->type($T_DEL);
1032              
1033 29         1247 my $is_closed = $item->closed(0);
1034 29         631 my $is_deleted = $item->type($T_DEL);
1035              
1036 29 100 66     1210 if ($was_closed == $is_closed && $was_deleted == $is_deleted) {
1037 5         14 return 0;
1038             }
1039              
1040 24         48 return 1;
1041             }
1042              
1043             =head2 remove
1044              
1045             Wipe out directory item designated by an offset entirely:
1046              
1047             my $num_removed = $dir->remove(index => $index);
1048              
1049             Wipe out directory item being the first one to match given PETSCII pattern entirely:
1050              
1051             use Text::Convert::PETSCII qw(:convert);
1052              
1053             my $pattern = ascii_to_petscii 'workstage*';
1054              
1055             my $num_removed = $dir->remove(pattern => $pattern, global => 0);
1056              
1057             Wipe out all directory items matching given PETSCII pattern entirely:
1058              
1059             use Text::Convert::PETSCII qw(:convert);
1060              
1061             my $pattern = ascii_to_petscii 'workstage*';
1062              
1063             my $num_removed = $dir->remove(pattern => $pattern, global => 1);
1064              
1065             C is expected to be any valid PETSCII text string. C parameter defaults to C<0>, hence removing only a single file matching given criteria by default. When set to any C value, it will trigger removal of B items with filename matching given PETSCII pattern.
1066              
1067             A call to this method always returns the number of successfully removed items.
1068              
1069             Note that this method removes an item from directory layout completely. It works a little bit like C, Perl's core method, removing a single element designated by an offset from an array of disk directory items, however it does not replace it with any new elements, it just shifts the remaining items, shortening a directory listing by one and moving everything from a given offset down. In order to safely mark given file as deleted without removing it from a directory listing, see description of L method.
1070              
1071             =cut
1072              
1073             sub remove {
1074 39     39 1 2655 my ($self, %args) = @_;
1075              
1076 39 100 100     142 if (exists $args{index} && exists $args{pattern}) {
1077 1         12 die q{Unable to entirely remove directory item: ambiguous removal index/pattern specified (you cannot specify both parameters at the same time)};
1078             }
1079              
1080 38 100 100     166 unless (exists $args{index} || exists $args{pattern}) {
1081 1         13 die q{Unable to entirely remove directory item: Missing index/pattern parameter (which element did you want to remove?)};
1082             }
1083              
1084 37         64 my $index = $args{index};
1085 37         69 my $global = $args{global};
1086 37         71 my $pattern = $args{pattern};
1087              
1088 37         89 my $num_items = $self->num_items();
1089 37         1750 my $items = $self->{items};
1090              
1091 37 100       94 if (exists $args{index}) {
1092              
1093 10         52 $self->_validate_index($index, 'remove');
1094              
1095 5 100       12 if ($index < $num_items) {
1096 4         16 $self->_remove_item($index);
1097 4         15 return 1;
1098             }
1099             else {
1100 1         5 return 0;
1101             }
1102             }
1103             else {
1104              
1105 27         134 $self->_validate_pattern($pattern, 'remove');
1106              
1107 22         39 my $num_deleted = 0;
1108              
1109 22         66 for (my $i = 0; $i < $num_items; $i++) {
1110              
1111 40         1319 my $item = $items->[$i];
1112              
1113 40 100       116 if ($item->match_name($pattern)) {
1114              
1115 24         3040 $self->_remove_item($i);
1116              
1117 24         41 $num_deleted += 1;
1118              
1119             # File got deleted and only one was requested to get deleted:
1120 24 100       92 last unless $global;
1121              
1122 11         19 $i--;
1123 11         52 $num_items--;
1124             }
1125             }
1126              
1127 22         749 return $num_deleted;
1128             }
1129             }
1130              
1131             sub _remove_item {
1132 28     28   59 my ($self, $index) = @_;
1133              
1134 28         56 my $items = $self->{items};
1135              
1136 28         37 splice @{$items}, $index, 1;
  28         136  
1137              
1138 28         55 CORE::push @{$items}, D64::Disk::Dir::Item->new();
  28         86  
1139              
1140 28         2504 return undef;
1141             }
1142              
1143             =head2 add
1144              
1145             Add a new directory item to a directory listing:
1146              
1147             my $is_success = $dir->add(item => $item);
1148              
1149             Add a new directory item designated by an offset:
1150              
1151             my $is_success = $dir->add(item => $item, index => $index);
1152              
1153             C<$item> is expected to be a valid C object.
1154              
1155             A call to this method returns true on a successful addition of a new entry, and false otherwise. Addition of a new item may not be possible, for instance when a maximum number of allowed disk directory elements has already been reached.
1156              
1157             C<$index> indicates an offset from the beginning of a directory listing where a new item should be added, with count starting from C<0>. Note that this method will not only insert a new item into a disk directory, it will also shift the remaining items, extending a directory listing by one and moving everything from a given offset up. When C<$index> indicates an element beyond the number of non-empty items currently stored in a disk directory, subroutine will fail and an undefined value will be returned, because such operation would not make much sense (such added entry would not be obtainable from a directory listing anyway). It will also not work when number of non-empty items stored in a disk directory has already reached its maximum. Please note that this operation will not replace a "*", or "splat" file it encounters at a given offset, rather it will always it altogether with the remaining items, unlike C method called without an C parameter specified at all, which is described in the next paragraph.
1158              
1159             When C<$index> parameter is unspecified, the method behaves as follows. It finds the first empty slot in a directory listing (that is a first directory item with a "closed" flag unset), and writes given item at that exact position. It will however not work when there is no writable slot in a directory listing available at all. Please note that this operation may or may not write given item at the end of a directory listing, since it will replace any "*", or "splat" file it encounters earlier on its way. In most cases this is a desired behaviour, that is why it is always performed as a default action.
1160              
1161             =cut
1162              
1163             sub add {
1164 24     24 1 11711 my ($self, %args) = @_;
1165              
1166 24 100       440 unless (exists $args{item}) {
1167 2         22 die q{Unable to add an item to a directory listing: Missing item parameter (what element did you want to add?)};
1168             }
1169              
1170 22         53 my $index = $args{index};
1171 22         37 my $item = $args{item};
1172              
1173 22         90 $self->_validate_item_object($item, 'add');
1174              
1175 20         58 my $num_items = $self->num_items();
1176 20         890 my $items = $self->{items};
1177              
1178 20 100       56 unless (defined $index) {
1179 8         58 my $first_empty_slot = $self->_find_first_empty_slot();
1180              
1181 8 50       26 if (defined $first_empty_slot) {
1182 8         16 splice @{$items}, $first_empty_slot, 0x01, $item->clone();
  8         274  
1183 8         49 return 1;
1184             }
1185             }
1186             else {
1187 12         49 $self->_validate_index($index, 'add');
1188              
1189 8 100       38 if ($num_items >= $MAX_ENTRIES) {
1190 1         11 warn sprintf q{Unable to add another item to a directory listing, maximum number of %d entries has been reached}, $MAX_ENTRIES;
1191             }
1192              
1193 8 100       117 if ($index <= $num_items) {
1194 7         14 splice @{$items}, $index, 0x00, $item->clone();
  7         397  
1195 7         20 CORE::pop @{$items};
  7         25  
1196 7         44 return 1;
1197             }
1198             }
1199              
1200 1         4 return 0;
1201             }
1202              
1203             sub _find_first_empty_slot {
1204 8     8   21 my ($self) = @_;
1205              
1206 8         17 my $items = $self->{items};
1207              
1208 8         24 my $index = 0;
1209              
1210 8         41 while ($index < $MAX_ENTRIES) {
1211 18         102 my $item = $items->[$index];
1212 18 100       42 if ($item->writable()) {
1213 8         242 return $index;
1214             }
1215 10         168 $index++;
1216             }
1217              
1218 0         0 return undef;
1219             }
1220              
1221             =head2 put
1222              
1223             Put an item to a directory listing at any given position:
1224              
1225             my $is_success = $dir->put(item => $item, index => $index);
1226              
1227             C<$item> is expected to be a valid C object. A call to this method returns true on a successful put of a new entry, and false otherwise.
1228              
1229             C<$index> is a required parameter that indicates an offset from the beginning of a directory listing where a new item should be put, with count starting from C<0>. Note that this method does not just insert a new item into a disk directory, it rather replaces an existing item previously stored at a given offset. When C<$index> indicates an element beyond the number of non-empty items currently stored in a disk directory, subroutine will fail and an undefined value will be returned, because such operation would not make much sense (such added entry would not be obtainable from a directory listing anyway).
1230              
1231             =cut
1232              
1233             sub put {
1234 22     22 1 10551 my ($self, %args) = @_;
1235              
1236 22 100       83 unless (exists $args{index}) {
1237 1         15 die q{Unable to put an item to a directory listing: Missing index parameter (where did you want to put it?)};
1238             }
1239 21 100       61 unless (exists $args{item}) {
1240 1         11 die q{Unable to put an item to a directory listing: Missing item parameter (what did you want to put there?)};
1241             }
1242              
1243 20         38 my $index = $args{index};
1244 20         42 my $item = $args{item};
1245              
1246 20         78 $self->_validate_index($index, 'put');
1247 15         69 $self->_validate_item_object($item, 'put');
1248              
1249 13         39 my $num_items = $self->num_items();
1250 13         585 my $items = $self->{items};
1251              
1252 13 100       41 if ($index <= $num_items) {
1253 12         413 $items->[$index] = $item->clone();
1254 12         60 return 1;
1255             }
1256              
1257 1         5 return 0;
1258             }
1259              
1260             =head2 print
1261              
1262             Print out formatted disk directory listing:
1263              
1264             $dir->print(fh => $fh, as_petscii => $as_petscii);
1265              
1266             C<$fh> defaults to the standard output. C defaults to false (meaning that ASCII characters will be printed out by default).
1267              
1268             A printout does not include header and number of blocks free lines, because information about disk title, disk ID and number of free sectors is stored in a Block Availability Map (see L for more details on how to access these bits of information).
1269              
1270             =cut
1271              
1272             sub print {
1273 5     5 1 448 my ($self, %args) = @_;
1274              
1275 5   33     31 my $fh = $args{fh} || *STDOUT;
1276 5   100     61 my $as_petscii = $args{as_petscii} || 0;
1277              
1278 5         22 $fh->binmode(':bytes');
1279 5         35 my $stdout = select $fh;
1280              
1281 5         15 my $items = $self->{items};
1282 5         17 my $num_items = $self->num_items();
1283              
1284 5         251 for (my $i = 0; $i < $num_items; $i++) {
1285 30         5978 my $item = $items->[$i];
1286 30         71 $item->print(fh => $fh, as_petscii => $as_petscii);
1287             }
1288              
1289 5         904 select $stdout;
1290              
1291 5         19 return undef;
1292             }
1293              
1294             sub is_numeric {
1295 29     29 0 72 my ($self, $var) = @_;
1296              
1297 29         100 my $is_numeric = _is_numeric($var);
1298              
1299 29         82 return $is_numeric;
1300             }
1301              
1302             sub set_iok {
1303 416     416 0 1715285 my ($self, $var) = @_;
1304              
1305 416         1265 my $var_iok = _set_iok($var);
1306              
1307 416         958 return $var_iok;
1308             }
1309              
1310             sub _is_valid_byte_value {
1311 202778     202778   313282 my ($self, $byte_value) = @_;
1312              
1313 202778 50 66     705009 if (length ($byte_value) == 1 && ord ($byte_value) >= 0x00 && ord ($byte_value) <= 0xff) {
      66        
1314 202776         434348 return 1;
1315             }
1316              
1317 2         6 return 0;
1318             }
1319              
1320             sub _is_valid_number_value {
1321 61     61   128 my ($self, $number_value) = @_;
1322              
1323 61 100 100     177 if (D64::Disk::Dir::Item->is_int($number_value) && $number_value >= 0x00 && $number_value <= 0xff) {
      100        
1324 5         61 return 1;
1325             }
1326              
1327 56         501 return 0;
1328             }
1329              
1330             sub _dump {
1331 29     29   66 my ($self, $value) = @_;
1332              
1333 29 50       66 if ($self->_is_valid_number_value($value)) {
1334 0         0 return sprintf q{$%02x}, $value;
1335             }
1336              
1337 29 100       126 if ($self->is_numeric($value)) {
1338 11         349 return sprintf q{%s}, $value;
1339             }
1340              
1341 18         137 my $dump = Data::Dumper->new([$value])->Indent(0)->Terse(1)->Deepcopy(1)->Sortkeys(1)->Dump();
1342              
1343 18         1877 return $dump;
1344             }
1345              
1346             =head1 BUGS
1347              
1348             There are no known bugs at the moment. Please report any bugs or feature requests.
1349              
1350             =head1 EXPORT
1351              
1352             None. No method is exported into the caller's namespace neither by default nor explicitly.
1353              
1354             =head1 SEE ALSO
1355              
1356             L, L, L, L, L, L.
1357              
1358             =head1 AUTHOR
1359              
1360             Pawel Krol, Epawelkrol@cpan.orgE.
1361              
1362             =head1 VERSION
1363              
1364             Version 0.06 (2021-01-18)
1365              
1366             =head1 COPYRIGHT AND LICENSE
1367              
1368             Copyright 2013-2021 by Pawel Krol Epawelkrol@cpan.orgE.
1369              
1370             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.
1371              
1372             PLEASE NOTE THAT IT COMES WITHOUT A WARRANTY OF ANY KIND!
1373              
1374             =cut
1375              
1376             1;
1377              
1378             __END__