File Coverage

blib/lib/D64/Disk/Layout/Base.pm
Criterion Covered Total %
statement 186 224 83.0
branch 46 76 60.5
condition 15 51 29.4
subroutine 20 22 90.9
pod 7 7 100.0
total 274 380 72.1


line stmt bran cond sub pod time code
1             package D64::Disk::Layout::Base;
2              
3             =head1 NAME
4              
5             D64::Disk::Layout::Base - A base class for designing physical layouts of various Commodore disk image formats
6              
7             =head1 SYNOPSIS
8              
9             package D64::MyLayout;
10              
11             # Establish an ISA relationship with base class:
12             use base qw(D64::Disk::Layout::Base);
13              
14             # Number of bytes per sector storage:
15             our $bytes_per_sector = 256;
16              
17             # Number of sectors per track storage:
18             our @sectors_per_track = ( 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, # tracks 1-17
19             19, 19, 19, 19, 19, 19, 19, # tracks 18-24
20             18, 18, 18, 18, 18, 18, # tracks 25-30
21             17, 17, 17, 17, 17, 17, 17, 17, 17, 17 # tracks 31-40
22             );
23              
24             # Override default object constructor:
25             sub new {
26             my $class = shift;
27             my $self = $class->SUPER::new(@_);
28             if (defined $self) {
29             bless $self, $class;
30             return $self;
31             }
32             else {
33             warn 'Failed to create new D64::MyLayout object';
34             return undef;
35             }
36             }
37              
38             package main;
39              
40             # Read disk image data from file and create new derived class object instance:
41             my $diskLayoutObj = D64::MyLayout->new('image.d64');
42              
43             # Get number of tracks available for use:
44             my $num_tracks = $diskLayoutObj->num_tracks();
45             # Get number of sectors per track information:
46             my $num_sectors = $diskLayoutObj->num_sectors($track);
47              
48             # Read physical sector data from a disk image:
49             my $data = $diskLayoutObj->sector_data($track, $sector);
50             my @data = $diskLayoutObj->sector_data($track, $sector);
51              
52             # Write physical sector data into a disk image:
53             $diskLayoutObj->sector_data($track, $sector, $data);
54             $diskLayoutObj->sector_data($track, $sector, @data);
55              
56             # Read physical track data from a disk image:
57             my $data = $diskLayoutObj->track_data($track);
58             my @data = $diskLayoutObj->track_data($track);
59              
60             # Write physical track data into a disk image:
61             $diskLayoutObj->track_data($track, $data);
62             $diskLayoutObj->track_data($track, @data);
63              
64             # Save data changes to file:
65             $diskLayoutObj->save();
66             $diskLayoutObj->save_as('image.d64');
67              
68             =head1 DESCRIPTION
69              
70             This package provides a base class for designing physical layouts of various Commodore disk image formats, represented by data that can be allocated into tracks and sectors. The following two variables are required to be defined at a package-scope level of any derived class:
71              
72             our $bytes_per_sector = 256;
73              
74             This scalar value defines number of bytes per sector storage.
75              
76             our @sectors_per_track = ( 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, # tracks 1-17
77             19, 19, 19, 19, 19, 19, 19, # tracks 18-24
78             18, 18, 18, 18, 18, 18, # tracks 25-30
79             17, 17, 17, 17, 17, 17, 17, 17, 17, 17 # tracks 31-40
80             );
81              
82             This list defines number of sectors per track storage.
83              
84             Initialization of both these properties is always validated at compile-time within import method of the base class.
85              
86             =head1 METHODS
87              
88             =cut
89              
90 3     3   281288 use bytes;
  3         69  
  3         16  
91 3     3   99 use strict;
  3         7  
  3         61  
92 3     3   15 use warnings;
  3         6  
  3         89  
93              
94 3     3   15 use base qw(Exporter);
  3         6  
  3         679  
95             our %EXPORT_TAGS = ();
96             $EXPORT_TAGS{'all'} = [];
97             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
98             our @EXPORT = qw();
99              
100             our $VERSION = '0.03';
101              
102 3     3   24 use Carp qw(carp croak);
  3         6  
  3         7185  
103              
104             sub import {
105 0     0   0 my $this = shift;
106 0   0     0 my $class = ref($this) || $this;
107 0         0 my $bytes_per_sector = $class->_derived_class_property_value('$bytes_per_sector');
108 0 0       0 croak "Derived class \"${class}\" does not define \"\$bytes_per_sector\" value" unless defined $bytes_per_sector;
109 0         0 my $sectors_per_track_aref = $class->_derived_class_property_value('@sectors_per_track');
110 0 0       0 croak "Derived class \"${class}\" does not define \"\@sectors_per_track\" array" unless defined $sectors_per_track_aref;
111             # $class->_track_data_offsets($bytes_per_sector, $sectors_per_track_aref);
112 0         0 $class->SUPER::import();
113             }
114              
115             =head2 new
116              
117             Create empty unformatted disk image layout:
118              
119             my $diskLayoutObj = D64::Disk::Layout::Base->new();
120              
121             Read disk image layout from existing file:
122              
123             my $diskLayoutObj = D64::Disk::Layout::Base->new('image.d64');
124              
125             A valid D64::Disk::Layout::Base object is returned upon success, an undefined value otherwise.
126              
127             You are most likely wanting to override this method in your derived class source code by calling it first to create an object and then reblessing a referenced object currently belonging to the base class:
128              
129             use base qw(D64::Disk::Layout::Base);
130              
131             sub new {
132             my $class = shift;
133             my $self = $class->SUPER::new(@_);
134             if (defined $self) {
135             bless $self, $class;
136             return $self;
137             }
138             else {
139             warn 'Failed to create new D64::MyLayout object';
140             return undef;
141             }
142             }
143              
144             Creating a new object may fail upon one of the following conditions:
145              
146             =over
147              
148             =item *
149             File specified as an input parameter does not exist or cannot be read
150              
151             =item *
152             File is too short, what causes inability to read complete sector data
153              
154             =back
155              
156             =cut
157              
158             sub new {
159 23     23 1 2012200 my $this = shift;
160 23   33     126 my $class = ref($this) || $this;
161 23         50 my $self = {};
162 23         49 bless $self, $class;
163 23         68 my $initOK = $self->_initialize(@_);
164 23 50       55 if ($initOK) {
165 23         65 return $self;
166             }
167             else {
168 0         0 return undef;
169             }
170             }
171              
172             sub _initialize {
173 23     23   37 my $self = shift;
174 23         39 my $filename = shift;
175 23 100       62 if (defined $filename) {
176             # Validate that file exists:
177 2 50       68 unless (-e $filename) {
178 0         0 carp "File \"${filename}\" does not exist";
179 0         0 return 0;
180             }
181 2 50       38 unless (-r $filename) {
182 0         0 carp "Unable to open file \"${filename}\" for reading";
183 0         0 return 0;
184             }
185             # Read disk image data from file:
186 2         20 my $readOK = $self->_read_image_data($filename);
187 2 50       14 return 0 unless $readOK;
188             }
189             else {
190             # Create new empty disk image:
191 21         50 $self->_create_empty_image();
192             }
193 23         50 return 1;
194             }
195              
196             sub _create_empty_image {
197 21     21   32 my $self = shift;
198 21   33     56 my $class = ref($self) || $self;
199 21         61 my $bytes_per_sector = $class->_derived_class_property_value('$bytes_per_sector');
200 21         68 my $sectors_per_track_aref = $class->_derived_class_property_value('@sectors_per_track');
201             # Generate track data:
202 21         42 my $num_tracks = @{$sectors_per_track_aref};
  21         42  
203 21         66 for (my $track = 1; $track <= $num_tracks; $track++) {
204             # Generate sector data:
205 78         161 my $num_sectors = $sectors_per_track_aref->[$track - 1];
206 78         160 for (my $sector = 0; $sector < $num_sectors; $sector++) {
207 174         338 my $buffer = chr (0x00) x $bytes_per_sector;
208 174         379 $self->sector_data($track, $sector, $buffer);
209             }
210             }
211             }
212              
213             sub _read_image_data {
214 2     2   8 my $self = shift;
215 2         6 my $filename = shift;
216 2   33     11 my $class = ref($self) || $self;
217 2         11 my $bytes_per_sector = $class->_derived_class_property_value('$bytes_per_sector');
218 2         11 my $sectors_per_track_aref = $class->_derived_class_property_value('@sectors_per_track');
219             # my $track_data_offsets_aref = $class->_derived_class_property_value('@track_data_offsets');
220             # Open file for reading:
221 2 50       123 open (my $fh, '<', $filename) or croak $!;
222 2         36 binmode $fh;
223             # Read track data:
224 2         5 my $num_tracks = @{$sectors_per_track_aref};
  2         8  
225 2         14 for (my $track = 1; $track <= $num_tracks; $track++) {
226             # Read sector data:
227 10         29 my $num_sectors = $sectors_per_track_aref->[$track - 1];
228 10         41 for (my $sector = 0; $sector < $num_sectors; $sector++) {
229 24         45 my $buffer;
230             # my $offset = $track_data_offsets_aref->[$track - 1] + $sector * $bytes_per_sector;
231 24         324 my $num_bytes = sysread ($fh, $buffer, $bytes_per_sector);
232 24 50 0     104 if ($num_bytes == $bytes_per_sector) {
    0          
233 24         99 $self->sector_data($track, $sector, $buffer);
234             }
235             elsif ($num_bytes > 0 and $num_bytes != $bytes_per_sector) {
236 0         0 croak "Number of bytes read from disk image \"${filename}\" on track ${track} and sector ${sector} is ${num_bytes} when ${bytes_per_sector} bytes were expected (file too short?)";
237             }
238             }
239             }
240             # Close file upon reading:
241 2 50       38 close ($fh) or croak $!;
242             # Keep the name of file read for further data saving actions:
243 2         23 $self->{'FILE'} = $filename;
244             }
245              
246             =head2 sector_data
247              
248             Read physical sector data from a disk image:
249              
250             my $data = $diskLayoutObj->sector_data($track, $sector);
251             my @data = $diskLayoutObj->sector_data($track, $sector);
252              
253             Can either be read into a scalar (in which case it is a bytes sequence) or into an array (method called in a list context returns a list of single bytes of data). Length of a scalar as well as size of an array depends on number of bytes per sector storage defined within derived class in $bytes_per_sector variable.
254              
255             A valid sector data is returned upon successful read, an undefined value otherwise.
256              
257             Write physical sector data into a disk image:
258              
259             $diskLayoutObj->sector_data($track, $sector, $data);
260             $diskLayoutObj->sector_data($track, $sector, @data);
261              
262             Same as above, data to write can be provided as a scalar (a bytes sequence of strictly defined length) as well as an array (list of single bytes of data of precisely specified size).
263              
264             A valid sector data is returned upon successful write, an undefined value otherwise.
265              
266             =cut
267              
268             sub sector_data {
269 266     266 1 635 my $self = shift;
270 266         403 my $track = shift;
271 266         364 my $sector = shift;
272 266         663 my @data = splice @_;
273 266   33     654 my $class = ref($self) || $self;
274 266         456 my $data;
275 266         676 $data .= $_ for @data;
276 266 50       610 return unless $class->_valid_track_number($track);
277 266 50       701 return unless $self->_valid_sector_number($track, $sector);
278 266 100       552 if (defined $data) {
279 203         629 $class->_validate_data_length(\$data, 1);
280 203         665 $class->_pad_data_with_zeroes(\$data, 1);
281 203         658 $self->{'DATA'}->[$track]->[$sector] = $data;
282             }
283 266 100       999 return unless defined wantarray;
284 68         141 $data = $self->{'DATA'}->[$track]->[$sector];
285 68 100       135 if (wantarray) {
286 24         68 @data = split //, $data;
287 24         101 return @data;
288             }
289             else {
290 44         122 return $data;
291             }
292             }
293              
294             =head2 track_data
295              
296             Read physical track data from a disk image:
297              
298             my $data = $diskLayoutObj->track_data($track);
299             my @data = $diskLayoutObj->track_data($track);
300              
301             Can either be read into a scalar (in which case it is a bytes sequence) or into an array (method called in a list context returns a list of single bytes of data). Length of a scalar as well as size of an array depend on number of bytes per sector storage defined within derived class in $bytes_per_sector attribute and number of sectors per track storage defined within derived class in @sectors_per_track property.
302              
303             A valid track data is returned upon successful read, an undefined value otherwise.
304              
305             Write physical track data into a disk image:
306              
307             $diskLayoutObj->track_data($track, $data);
308             $diskLayoutObj->track_data($track, @data);
309              
310             Same as above, data to write can be provided as a scalar (a bytes sequence of strictly defined length) as well as an array (list of single bytes of data of precisely specified size).
311              
312             A valid track data is returned upon successful write, an undefined value otherwise.
313              
314             =cut
315              
316             sub track_data {
317 4     4 1 1046 my $self = shift;
318 4         7 my $track = shift;
319 4         10 my @data = splice @_;
320 4   33     12 my $class = ref ($self) || $self;
321 4         7 my $data;
322 4         11 $data .= $_ for @data;
323 4 50       15 return unless $class->_valid_track_number($track);
324 4         11 my $num_sectors = $self->num_sectors($track);
325 4 100       12 if (defined $data) {
326 3         9 $class->_validate_data_length(\$data, $num_sectors);
327 3         43 $class->_pad_data_with_zeroes(\$data, $num_sectors);
328 3         77 my $bytes_per_sector = $class->_derived_class_property_value('$bytes_per_sector');
329 3         11 for (my $sector = 0; $sector < $num_sectors; $sector++) {
330 9         17 my $sector_data = substr $data, $sector * $bytes_per_sector, $bytes_per_sector;
331 9         25 $self->{'DATA'}->[$track]->[$sector] = $sector_data;
332             }
333             }
334 4 50       11 return unless defined wantarray;
335 4         7 $data = join '', @{$self->{'DATA'}->[$track]};
  4         12  
336 4 50       10 if (wantarray) {
337 4         18 @data = split //, $data;
338 4         22 return @data;
339             }
340             else {
341 0         0 return $data;
342             }
343             }
344              
345             sub _track_data_offsets {
346 0     0   0 my ($class, $bytes_per_sector, $sectors_per_track_aref) = splice @_;
347 0         0 my @track_data_offsets = ();
348 0         0 my $offset = 0;
349 0         0 my $num_tracks = @{$sectors_per_track_aref};
  0         0  
350 0         0 for (my $track = 0; $track < $num_tracks; $track++) {
351 0         0 push @track_data_offsets, $offset;
352 0         0 $offset += $sectors_per_track_aref->[$track] * $bytes_per_sector;
353             }
354 0         0 $class->_derived_class_property_value('@track_data_offsets', \@track_data_offsets);
355             }
356              
357             sub _derived_class_property_value {
358 1020     1020   1635 my $this = shift;
359 1020         1586 my $param = shift;
360 1020         1483 my $value = shift;
361 1020   33     2907 my $class = ref($this) || $this;
362 1020         3802 $param =~ s/^(.)//;
363 1020         2488 my $type = $+;
364 1020 100       2492 if ($type eq '$') {
    50          
365 442 50       747 unless (defined $value) {
366 442         17653 return eval "\$${class}::${param}";
367             }
368             else {
369 0         0 return eval "\$${class}::${param} = \$value";
370             }
371             }
372             elsif ($type eq '@') {
373 578 50       1058 unless (defined $value) {
374 578         25890 return eval "\\\@${class}::${param}";
375             }
376             else {
377 0         0 return eval "\@${class}::${param} = \@{\$value}";
378             }
379             }
380 0         0 return undef;
381             }
382              
383             sub _valid_track_number {
384 270     270   510 my ($class, $track) = @_;
385             # Validate track number (should be within range 1 .. $num_tracks):
386 270         564 my $sectors_per_track_aref = $class->_derived_class_property_value('@sectors_per_track');
387 270         552 my $num_tracks = @{$sectors_per_track_aref};
  270         471  
388 270 50 33     1101 if ($track < 1 or $track > $num_tracks) {
389 0         0 carp "Invalid track number: ${track} (accepted track number range for this class is: 1 <= \$track <= ${num_tracks})";
390 0         0 return 0;
391             }
392 270         672 return 1;
393             }
394              
395             sub _valid_sector_number {
396 266     266   489 my ($self, $track, $sector) = @_;
397             # Validate sector number (should be within range 0 .. $num_sectors - 1):
398 266         552 my $num_sectors = $self->num_sectors($track);
399 266 50 33     900 if ($sector < 0 or $sector >= $num_sectors) {
400 0         0 carp "Invalid sector number: ${sector} (accepted sector number range for this class is: 0 <= \$sector < ${num_sectors})";
401 0         0 return 0;
402             }
403 266         649 return 1;
404             }
405              
406             sub _validate_data_length {
407 206     206   401 my ($class, $data_ref, $num_sectors) = @_;
408 206         378 my $bytes_per_sector = $class->_derived_class_property_value('$bytes_per_sector');
409 206         412 my $data_length = length ${$data_ref};
  206         405  
410 206         348 my $data_length_wanted = $bytes_per_sector * $num_sectors;
411             # Validate data length (should contain exactly "$bytes_per_sector" times "$num_sectors" bytes):
412 206 100       577 if ($data_length > $data_length_wanted) {
413 2         5 my $bytes_truncated = $data_length - $data_length_wanted;
414 2         4 substr ${$data_ref}, $data_length_wanted, $bytes_truncated, '';
  2         12  
415 2 100       12 my $what = $num_sectors == 1 ? 'sector' : 'track';
416 2         202 carp "Too much data provided while writing physical ${what} into disk image, last ${bytes_truncated} bytes of data truncated and just ${data_length_wanted} bytes written";
417             }
418             }
419              
420             sub _pad_data_with_zeroes {
421 206     206   406 my ($class, $data_ref, $num_sectors) = @_;
422 206         390 my $bytes_per_sector = $class->_derived_class_property_value('$bytes_per_sector');
423 206         407 my $data_length = length ${$data_ref};
  206         387  
424 206         337 my $data_length_wanted = $bytes_per_sector * $num_sectors;
425             # Pad data to be written to disk with zeroes (uninitialized values):
426 206 100       500 if ($data_length < $data_length_wanted) {
427 2         4 my $bytes_appended = $data_length_wanted - $data_length;
428 2         3 substr ${$data_ref}, $data_length, 0, chr (0x00) x $bytes_appended;
  2         9  
429 2 100       9 my $what = $num_sectors == 1 ? 'sector' : 'track';
430 2         326 carp "Too little data provided while writing physical ${what} into disk image, ${bytes_appended} extra zero bytes of data appended and ${data_length_wanted} bytes written";
431             }
432             }
433              
434             =head2 num_tracks
435              
436             Get number of tracks available:
437              
438             my $num_tracks = $diskLayoutObj->num_tracks();
439              
440             =cut
441              
442             sub num_tracks {
443 2     2 1 24 my $self = shift;
444 2   33     7 my $class = ref($self) || $self;
445 2         5 my $sectors_per_track_aref = $class->_derived_class_property_value('@sectors_per_track');
446 2         6 my $num_tracks = @{$sectors_per_track_aref};
  2         4  
447 2         6 return $num_tracks;
448             }
449              
450             =head2 num_sectors
451              
452             Get number of sectors per track:
453              
454             my $num_sectors = $diskLayoutObj->num_sectors($track);
455              
456             Number of sectors per specified track is returned upon success, an undefined value otherwise.
457              
458             =cut
459              
460             sub num_sectors {
461 279     279 1 511 my $self = shift;
462 279         392 my $track = shift;
463 279   33     665 my $class = ref($self) || $self;
464 279         572 my $sectors_per_track_aref = $class->_derived_class_property_value('@sectors_per_track');
465 279         568 my $num_tracks = @{$sectors_per_track_aref};
  279         455  
466 279 50 33     1086 if ($track < 1 or $track > $num_tracks) {
467 0         0 carp "Invalid track number: ${track} (accepted track number range for this class is: 1 <= \$track <= ${num_tracks})";
468 0         0 return undef;
469             }
470 279         601 my $num_sectors = $sectors_per_track_aref->[$track - 1];
471 279         511 return $num_sectors;
472             }
473              
474             =head2 save
475              
476             Save disk layout data to previously loaded image file:
477              
478             my $saveOK = $diskLayoutObj->save();
479              
480             This method will not work when layout object is created as an empty unformatted disk image. Creating empty unformatted disk image layout forces usage of "save_as" method to save data by providing a filename to create new file. Disk layout object needs to be created by reading disk image layout from existing file to make this particular subroutine operative.
481              
482             Returns true value upon successful write, and false otherwise.
483              
484             =cut
485              
486             sub save {
487 2     2 1 897 my $self = shift;
488 2         13 my $filename = $self->{'FILE'};
489 2 100       10 unless (defined $filename) {
490 1         217 carp "This disk layout object has been created as an empty unformatted disk image without a filename specified during its creation. You need to use 'save_as' method in order to provide a filename to create new file instead";
491 1         85 return 0;
492             }
493 1         6 my $saveOK = $self->save_as($filename);
494 1         6 return $saveOK;
495             }
496              
497             =head2 save_as
498              
499             Save disk layout data to file with specified name:
500              
501             my $saveOK = $diskLayoutObj->save_as('image.d64');
502              
503             A behaviour implemented in this method prevents from overwriting an existing file unless it is the same file as the one that data has been previously read from (the same file that was used while creating this object instance).
504              
505             Returns true value upon successful write, and false otherwise.
506              
507             =cut
508              
509             sub save_as {
510 4     4 1 309 my $self = shift;
511 4         10 my $filename = shift;
512 4   33     16 my $class = ref($self) || $self;
513             # Test if provided filename is the same as file loaded during initialization:
514 4         11 my $loaded_filename = $self->{'FILE'};
515 4 100 66     28 unless (defined $loaded_filename and $loaded_filename eq $filename) {
516             # Validate that target file does not exist yet:
517 3 50       141 if (-e $filename) {
518 0         0 carp "Unable to save disk layout data. Target file \"${filename}\" already exists";
519 0         0 return 0;
520             }
521             }
522             # If both names are the same, there is no need to validate file existence,
523             # because in such case we allow to overwrite original file with new data!
524 4         22 my $bytes_per_sector = $class->_derived_class_property_value('$bytes_per_sector');
525 4         19 my $sectors_per_track_aref = $class->_derived_class_property_value('@sectors_per_track');
526             # Open file for writing:
527 4 50       461 open (my $fh, '>', $filename) or croak $!;
528 4         24 binmode $fh;
529             # Write track data:
530 4         11 my $num_tracks = @{$sectors_per_track_aref};
  4         17  
531 4         23 for (my $track = 1; $track <= $num_tracks; $track++) {
532             # Write sector data:
533 18         42 my $num_sectors = $sectors_per_track_aref->[$track - 1];
534 18         85 for (my $sector = 0; $sector < $num_sectors; $sector++) {
535 42         129 my $data = $self->sector_data($track, $sector);
536             # my $offset = $track_data_offsets_aref->[$track - 1] + $sector * $bytes_per_sector;
537 42         790 my $num_bytes = syswrite ($fh, $data, $bytes_per_sector);
538 42 50 33     290 unless (defined $num_bytes and $num_bytes == $bytes_per_sector) {
539 0         0 carp "There was a problem writing data to file \"${filename}\": $!";
540 0         0 close $fh;
541 0 0 0     0 unlink $filename if defined $loaded_filename and $loaded_filename ne $filename;
542 0         0 return 0;
543             }
544             }
545             }
546             # Close file upon reading:
547 4 50       144 close ($fh) or croak $!;
548             # Keep the name of file read for further data saving actions:
549 4         17 $self->{'FILE'} = $filename;
550 4         25 return 1;
551             }
552              
553             =head1 BUGS
554              
555             There are no known bugs at the moment. Please report any bugs or feature requests.
556              
557             =head1 EXPORT
558              
559             None. No method is exported into the caller's namespace either by default or explicitly.
560              
561             =head1 SEE ALSO
562              
563             L
564              
565             =head1 AUTHOR
566              
567             Pawel Krol, Epawelkrol@cpan.orgE.
568              
569             =head1 VERSION
570              
571             Version 0.03 (2021-01-12)
572              
573             =head1 COPYRIGHT AND LICENSE
574              
575             Copyright 2011-2021 by Pawel Krol .
576              
577             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
578              
579             =cut
580              
581             1;