File Coverage

blib/lib/D64/Disk/BAM.pm
Criterion Covered Total %
statement 337 365 92.3
branch 84 92 91.3
condition 46 57 80.7
subroutine 37 39 94.8
pod 16 16 100.0
total 520 569 91.3


line stmt bran cond sub pod time code
1             package D64::Disk::BAM;
2              
3             =head1 NAME
4              
5             D64::Disk::BAM - Processing the BAM (Block Availability Map) area of the Commodore disk images (D64 format only)
6              
7             =head1 SYNOPSIS
8              
9             use D64::Disk::BAM;
10              
11             # Create new empty BAM object:
12             my $diskBAM = D64::Disk::BAM->new();
13              
14             # Create new BAM object based on the BAM sector data retrieved from a D64 disk image file:
15             my $diskBAM = D64::Disk::BAM->new($sector_data);
16              
17             # Get the BAM sector data:
18             my $sector_data = $diskBAM->get_bam_data();
19              
20             # Clear the entire BAM sector data:
21             $diskBAM->clear_bam();
22              
23             # Get disk name converted to an ASCII string:
24             my $to_ascii = 1;
25             my $disk_name = $diskBAM->disk_name($to_ascii);
26              
27             # Set disk name converted from an ASCII string:
28             my $to_petscii = 1;
29             $diskBAM->disk_name($to_petscii, $disk_name);
30              
31             # Get full disk ID converted to an ASCII string:
32             my $to_ascii = 1;
33             my $full_disk_id = $diskBAM->full_disk_id($to_ascii);
34              
35             # Set full disk ID converted from an ASCII string:
36             my $to_petscii = 1;
37             $diskBAM->full_disk_id($to_petscii, $full_disk_id);
38              
39             # Get the number of free sectors on the specified track:
40             my $num_free_sectors = $diskBAM->num_free_sectors($track);
41              
42             # Check if the sector is used:
43             my $is_sector_used = $diskBAM->sector_used($track, $sector);
44              
45             # Set specific sector to allocated:
46             $diskBAM->sector_used($track, $sector, 1);
47              
48             # Check if the sector is free:
49             my $is_sector_free = $diskBAM->sector_free($track, $sector);
50              
51             # Set specific sector to deallocated:
52             $diskBAM->sector_free($track, $sector, 1);
53              
54             # Write BAM layout textual representation to a file handle:
55             $diskBAM->print_out_bam_layout($fh);
56              
57             # Print out formatted disk header line to a file handle:
58             $diskBAM->print_out_disk_header($fh);
59              
60             # Print out number of free blocks line to a file handle:
61             $diskBAM->print_out_blocks_free($fh);
62              
63             =head1 DESCRIPTION
64              
65             Sector 0 of the directory track contains the BAM (Block Availability Map) and disk name/ID. This package provides the complete set of methods essential for accessing, managing and manipulating the contents of the BAM area of the Commodore disk images (note that only D64 format is supported).
66              
67             =head1 METHODS
68              
69             =cut
70              
71 6     6   508056 use bytes;
  6         56  
  6         45  
72 6     6   192 use strict;
  6         11  
  6         148  
73 6     6   30 use warnings;
  6         13  
  6         311  
74              
75             our $VERSION = '0.05';
76              
77 6     6   37 use Carp qw/carp croak/;
  6         34  
  6         489  
78 6     6   2607 use Text::Convert::PETSCII qw/:convert/;
  6         38650  
  6         968  
79              
80             # Track containing the entire directory:
81 6     6   55 use constant DIRECTORY_FIRST_TRACK => 0x00;
  6         15  
  6         345  
82             # First directory sector:
83 6     6   33 use constant DIRECTORY_FIRST_SECTOR => 0x01;
  6         16  
  6         301  
84             # Disk DOS version type:
85 6     6   37 use constant DISK_DOS_VERSION_TYPE => 0x02;
  6         12  
  6         300  
86             # Disk Name (padded with $A0):
87 6     6   47 use constant DISK_NAME => 0x90;
  6         14  
  6         419  
88             # Disk ID:
89 6     6   55 use constant DISK_ID => 0xa2;
  6         12  
  6         257  
90             # Full Disk ID:
91 6     6   32 use constant FULL_DISK_ID => 0xa2;
  6         21  
  6         282  
92             # DOS type, usually "2A":
93 6     6   36 use constant DOS_TYPE => 0xa5;
  6         21  
  6         23683  
94              
95             # Number of sectors per track storage:
96             our @SECTORS_PER_TRACK = (
97             21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, # tracks 1-17
98             19, 19, 19, 19, 19, 19, 19, # tracks 18-24
99             18, 18, 18, 18, 18, 18, # tracks 25-30
100             17, 17, 17, 17, 17, # tracks 31-35
101             );
102              
103             # BAM entries for each track (starting on track 1):
104             our @TRACK_BAM_ENTRIES = (
105             0x04, 0x08, 0x0c, 0x10, 0x14, 0x18, 0x1c, 0x20, 0x24, 0x28, 0x2c, 0x30, 0x34, 0x38, 0x3c, 0x40, 0x44, # tracks 1-17
106             0x48, 0x4c, 0x50, 0x54, 0x58, 0x5c, 0x60, # tracks 18-24
107             0x64, 0x68, 0x6c, 0x70, 0x74, 0x78, # tracks 25-30
108             0x7c, 0x80, 0x84, 0x88, 0x8c, # tracks 31-35
109             );
110              
111             our @SECTOR_BAM_OFFSETS = (
112             0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, # sectors 0-7
113             0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # sectors 8-15
114             0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, # sectors 16-23
115             );
116              
117             our @SECTOR_BAM_BITMASK = (
118             0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, # sectors 0-7
119             0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, # sectors 8-15
120             0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, # sectors 16-23
121             );
122              
123             =head2 new
124              
125             Create new empty BAM object:
126              
127             my $diskBAM = D64::Disk::BAM->new();
128              
129             Create new BAM object based on the BAM sector data:
130              
131             my $diskBAM = D64::Disk::BAM->new($sector_data);
132             my $diskBAM = D64::Disk::BAM->new(@sector_data);
133              
134             Upon failure an undefined value is returned.
135              
136             Be careful providing the right sector input data. C<$sector_data> is expected to be the stream of bytes. C<@sector_data> is expected to be the list of single bytes (not the numeric byte values!).
137              
138             =cut
139              
140             sub new {
141 79     79 1 71642 my $this = shift;
142 79   33     342 my $class = ref($this) || $this;
143 79         152 my $self = [];
144 79         161 bless $self, $class;
145 79         196 my $initOK = $self->_initialize(@_);
146 79 100       137 if ($initOK) {
147 75         218 return $self;
148             }
149             else {
150 4         30 return undef;
151             }
152             }
153              
154             sub _initialize {
155 79     79   120 my $self = shift;
156 79         174 my @sector_data = grep { defined } splice @_;
  36         122  
157 79         126 my $sector_data;
158 79         186 $self->_empty_bam();
159 79         166 $sector_data .= $_ for @sector_data;
160 79 100       192 if ($self->_setup_data($sector_data)) {
161 75         170 return 1;
162             }
163             else {
164 4         25 return 0;
165             }
166             }
167              
168             sub _setup_data {
169 79     79   132 my $self = shift;
170 79         114 my $sector_data = shift;
171 79 100       177 if ($sector_data) {
172 8 100       21 return 0 unless $self->_validate_bam_data($sector_data);
173 4         17 for (my $i = 0; $i < length ($sector_data); $i++) {
174 1024         1259 my $byte = substr $sector_data, $i, 1;
175 1024         1681 $self->[$i] = ord $byte;
176             }
177             }
178 75         171 return 1;
179             }
180              
181             sub _validate_bam_data {
182 8     8   11 my $self = shift;
183 8         15 my $sector_data = shift;
184             # Validate sector data, length is ok and all values correct!
185 8 100       21 if (length ($sector_data) != 256) {
186 1         149 carp sprintf q{Failed to validate the BAM sector data, expected the stream of 256 bytes but got %d bytes}, length ($sector_data);
187 1         9 return 0;
188             }
189 7         19 for (my $track = 1; $track <= @TRACK_BAM_ENTRIES; $track++) {
190 195         280 my $track_bam_index = $TRACK_BAM_ENTRIES[$track - 1];
191 195         241 my $track_num_sectors = $SECTORS_PER_TRACK[$track - 1];
192             # The first byte is the number of free sectors on that track:
193 195         266 my $num_free_sectors = ord substr $sector_data, $track_bam_index, 1;
194 195 100       305 if ($num_free_sectors > $track_num_sectors) {
195 2         438 carp sprintf q{Failed to validate the BAM sector data, invalid number of free sectors reported on track %d: claims %d sectors free but track %d has only %d sectors}, $track, $num_free_sectors, $track, $track_num_sectors;
196 2         18 return 0;
197             }
198             # The next three bytes represent the bitmap of which sectors are used/free:
199 193         412 my $free_sectors_bitmap = unpack 'b*', (substr $sector_data, $track_bam_index + 1, 3);
200             # Calculate the number of free sectors according to the bitmap allocation:
201 193         725 my $free_sectors_count = scalar grep { $_ == 1 } split //, $free_sectors_bitmap;
  4632         7121  
202             # The first byte that is the number of free sectors on that track and
203             # the next three bytes representing the bitmap of which sectors are
204             # used/free do not match, then this BAM sector data is invalid:
205 193 100       693 if ($free_sectors_count != $num_free_sectors) {
206 1         220 carp sprintf q{Failed to validate the BAM sector data, number of free sectors on track %d (which is claimed to be %d) does not match free sector allocation (which seems to be %d)}, $track, $num_free_sectors, $free_sectors_count;
207 1         11 return 0;
208             }
209             }
210 4         7 my $directory_first_track = ord substr $sector_data, DIRECTORY_FIRST_TRACK, 1;
211 4 100       12 if ($directory_first_track != 0x12) {
212 1         198 carp sprintf q{Warning! Track location of the first directory sector should be set to 18, but it is not: %d found in the BAM sector data}, $directory_first_track;
213             }
214 4         14 my $directory_first_sector = ord substr $sector_data, DIRECTORY_FIRST_SECTOR, 1;
215 4 100       10 if ($directory_first_sector != 0x01) {
216 1         234 carp sprintf q{Warning! Sector location of the first directory sector should be set to 1, but it is not: %d found in the BAM sector data}, $directory_first_sector;
217             }
218 4         17 my $section_filled_with_A0 = unpack 'h*', (substr $sector_data, 0xa0, 2);
219 4 100       13 if ($section_filled_with_A0 ne '0a0a') {
220 1         200 carp q{Warning! Bytes at offsets $A0-$A1 of the BAM sector data are expected to be filled with $A0, but they are not};
221             }
222 4         15 $section_filled_with_A0 = unpack 'h*', (substr $sector_data, 0xa7, 4);
223 4 100       12 if ($section_filled_with_A0 ne '0a0a0a0a') {
224 1         195 carp q{Warning! Bytes at offsets $A7-$AA of the BAM sector data are expected to be filled with $A0, but they are not};
225             }
226 4         17 return 1;
227             }
228              
229             sub _empty_bam {
230 80     80   123 my $self = shift;
231 80         114 my $disk_name = shift;
232 80         139 my $disk_id = shift;
233              
234 80         4037 $self->[$_] = ord chr 0x00 for 0x00 .. 0xff;
235              
236 80         149 $self->[DIRECTORY_FIRST_TRACK] = 0x12;
237 80         107 $self->[DIRECTORY_FIRST_SECTOR] = 0x01;
238 80         115 $self->[DISK_DOS_VERSION_TYPE] = 0x41;
239              
240 80         213 for (my $track = 1; $track <= @TRACK_BAM_ENTRIES; $track++) {
241 2800         4276 my $track_bam_index = $TRACK_BAM_ENTRIES[$track - 1];
242 2800         3809 my $track_num_sectors = $SECTORS_PER_TRACK[$track - 1];
243             # The first byte is the number of free sectors on that track:
244 2800         3555 $self->[$track_bam_index] = $track_num_sectors;
245             # The next three bytes represent the bitmap of which sectors are used/free:
246 2800         4632 my @free_sectors = $self->_track_bam_free_sectors($track);
247 2800         4827 @{$self}[$track_bam_index + 1 .. $track_bam_index + 3] = @free_sectors;
  2800         7280  
248             }
249              
250 80         504 $self->[DISK_NAME + $_] = 0xa0 for 0x00 .. 0x0f;
251              
252             # A0-A1: Filled with $A0
253 80         127 $self->[0xa0] = 0xa0;
254 80         118 $self->[0xa1] = 0xa0;
255             # A2-A3: Disk ID
256 80         164 $self->[0xa2] = 0xa0;
257 80         108 $self->[0xa3] = 0xa0;
258             # A4: Usually $A0
259 80         105 $self->[0xa4] = 0xa0;
260             # A5-A6: DOS type, usually "2A"
261 80         259 $self->[0xa5] = ord ascii_to_petscii '2';
262 80         2146 $self->[0xa6] = ord ascii_to_petscii 'a';
263             # A7-AA: Filled with $A0
264 80         1707 $self->[0xa7] = 0xa0;
265 80         111 $self->[0xa8] = 0xa0;
266 80         113 $self->[0xa9] = 0xa0;
267 80         137 $self->[0xaa] = 0xa0;
268             }
269              
270             sub _track_bam_free_sectors {
271 2800     2800   3665 my $self = shift;
272 2800         3364 my $track = shift;
273 2800         3431 my $free_sectors = 0;
274             # Get number of sectors per track storage:
275 2800         3593 my $num_sectors = $SECTORS_PER_TRACK[$track - 1];
276 2800         3775 my $bit = 1 << (24 - $num_sectors);
277 2800         4763 while ($num_sectors-- > 0) {
278 54640         62776 $free_sectors <<= 1;
279 54640         82724 $free_sectors |= $bit;
280             }
281 2800         6325 $free_sectors = sprintf q{%06x}, $free_sectors;
282 2800         11171 my @free_sectors = $free_sectors =~ m/(..)/g;
283 2800         4664 return map { _flip_bits(hex $_) } @free_sectors;
  8400         13557  
284             }
285              
286             sub _flip_bits {
287 8400     8400   11718 my ($byte) = @_;
288 8400         14339 my @lookup = (0x0, 0x8, 0x4, 0xc, 0x2, 0xa, 0x6, 0xe, 0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf);
289 8400         21543 return ($lookup[$byte & 0xf] << 4) | $lookup[$byte >> 4];
290             }
291              
292             =head2 clear_bam
293              
294             Clear the entire BAM sector data:
295              
296             $diskBAM->clear_bam();
297              
298             =cut
299              
300             sub clear_bam {
301 1     1 1 5 my $self = shift;
302 1         9 $self->_empty_bam();
303             }
304              
305             =head2 directory_first_track
306              
307             Get/set track location of the first directory sector (in theory it should be always set to 18, but it actually doesn't matter, and you should never trust what is here, you always use track/sector 18/1 for the first directory entry):
308              
309             $diskBAM->directory_first_track($directory_first_track);
310             my $directory_first_track = $diskBAM->directory_first_track();
311              
312             =cut
313              
314             sub directory_first_track {
315 7     7 1 15 my $self = shift;
316 7         10 my $directory_first_track = shift;
317 7 50       26 if (defined $directory_first_track) {
318 0         0 $self->[DIRECTORY_FIRST_TRACK] = $directory_first_track;
319             }
320 7         30 return $self->[DIRECTORY_FIRST_TRACK];
321             }
322              
323             =head2 directory_first_sector
324              
325             Get/set sector location of the first directory sector (in theory it should be always set to 1, but it actually doesn't matter, and you should never trust what is here, you always use track/sector 18/1 for the first directory entry):
326              
327             $diskBAM->directory_first_sector($directory_first_sector);
328             my $directory_first_sector = $diskBAM->directory_first_sector();
329              
330             =cut
331              
332             sub directory_first_sector {
333 1     1 1 5 my $self = shift;
334 1         10 my $directory_first_sector = shift;
335 1 50       5 if (defined $directory_first_sector) {
336 0         0 $self->[DIRECTORY_FIRST_SECTOR] = $directory_first_sector;
337             }
338 1         3 return $self->[DIRECTORY_FIRST_SECTOR];
339             }
340              
341             =head2 dos_version_type
342              
343             Get/set disk DOS version type:
344              
345             $diskBAM->dos_version_type($dos_version_type);
346             my $dos_version_type = $diskBAM->dos_version_type();
347              
348             When this byte is set to anything else than $41 or $00, we have what is called "soft write protection", thus any attempt to write to the disk will return the "DOS Version" error code 73, "CBM DOS V2.6 1541".
349              
350             =cut
351              
352             sub dos_version_type {
353 0     0 1 0 my $self = shift;
354 0         0 my $directory_first_sector = shift;
355 0 0       0 if (defined $directory_first_sector) {
356 0         0 $self->[DISK_DOS_VERSION_TYPE] = $directory_first_sector;
357             }
358 0         0 return $self->[DISK_DOS_VERSION_TYPE];
359             }
360              
361             =head2 get_bam_data
362              
363             Get the BAM sector data:
364              
365             my $sector_data = $diskBAM->get_bam_data();
366             my @sector_data = $diskBAM->get_bam_data();
367              
368             Depending on the context, either a reference or an array of bytes is returned.
369              
370             =cut
371              
372             sub get_bam_data {
373 8     8 1 57 my $self = shift;
374 8 50       17 if (wantarray) {
375 0         0 return @{$self};
  0         0  
376             }
377             else {
378 8         13 my $sector_data = q{};
379 8         13 $sector_data .= chr $_ for @{$self};
  8         377  
380 8         22 return $sector_data;
381             }
382             }
383              
384             =head2 disk_name
385              
386             Get disk name:
387              
388             my $disk_name = $diskBAM->disk_name($to_ascii);
389              
390             The first input parameter indicates whether value returned should get converted to an ASCII string upon retrieval:
391              
392             =over
393              
394             =item *
395             A false value defaults to the original 16-bytes long PETSCII string padded with $A0
396              
397             =item *
398             A true value enforces conversion of the original data to an ASCII string
399              
400             =back
401              
402             Set disk name:
403              
404             $diskBAM->disk_name($to_petscii, $disk_name);
405              
406             The first input parameter indicates whether C<$disk_name> parameter should get converted to a PETSCII string before storing:
407              
408             =over
409              
410             =item *
411             A false value indicates that C<$disk_name> has already been converted to a 16-bytes long PETSCII string and padded with $A0
412              
413             =item *
414             A true value enforces conversion of the original data to a valid PETSCII string
415              
416             =back
417              
418             Make sure that you either provide a valid PETSCII stream of bytes or use this option to get your original ASCII string properly converted.
419              
420             The second input parameter provides the actual disk name to be written to the BAM sector data.
421              
422             =cut
423              
424             sub disk_name {
425 30     30 1 11278 my $self = shift;
426 30         49 my $convert = shift;
427 30         45 my $disk_name = shift;
428 30 100       70 if (defined $disk_name) {
429 19         46 $self->_set_text_data(q{Disk name}, $disk_name, 16, $convert);
430             }
431 30         84 my $retrieved_disk_name = join '', map { chr } @{$self}[DISK_NAME .. DISK_NAME + 15];
  480         1092  
  30         66  
432             # Remove padded $A0 bytes at the end of a PETSCII string:
433 30         299 substr ($retrieved_disk_name, -1) = q{} while $retrieved_disk_name =~ m/\xa0$/;
434 30 100 100     183 if ((not defined $disk_name and $convert) or (defined $disk_name and not $convert)) {
      100        
      100        
435 9         31 $retrieved_disk_name = petscii_to_ascii($retrieved_disk_name);
436             }
437 30         1503 return $retrieved_disk_name;
438             }
439              
440             =head2 disk_id
441              
442             Get disk ID:
443              
444             my $disk_id = $diskBAM->disk_id($to_ascii);
445              
446             The first input parameter indicates whether value returned should get converted to an ASCII string upon retrieval:
447              
448             =over
449              
450             =item *
451             A false value defaults to the original 2-bytes long PETSCII string padded with $A0
452              
453             =item *
454             A true value enforces conversion of the original data to an ASCII string
455              
456             =back
457              
458             Set disk ID:
459              
460             $diskBAM->disk_id($to_petscii, $disk_id);
461              
462             The first input parameter indicates whether C<$disk_id> parameter should get converted to a PETSCII string before storing:
463              
464             =over
465              
466             =item *
467             A false value indicates that C<$disk_id> has already been converted to a 2-bytes long PETSCII string and padded with $A0
468              
469             =item *
470             A true value enforces conversion of the original data to a valid PETSCII string
471              
472             =back
473              
474             Make sure that you either provide a valid PETSCII stream of bytes or use this option to get your original ASCII string properly converted.
475              
476             The second input parameter provides the actual disk ID to be written to the BAM sector data.
477              
478             =cut
479              
480             sub disk_id {
481 12     12 1 2160 my $self = shift;
482 12         30 my $convert = shift;
483 12         30 my $disk_id = shift;
484 12 100       31 if (defined $disk_id) {
485 10         32 $self->_set_text_data(q{Disk ID}, $disk_id, 2, $convert);
486             }
487 12         29 my $retrieved_disk_id = join '', map { chr } @{$self}[DISK_ID .. DISK_ID + 1];
  24         58  
  12         27  
488             # Remove padded $A0 bytes at the end of a PETSCII string:
489 12         46 substr ($retrieved_disk_id, -1) = q{} while $retrieved_disk_id =~ m/\xa0$/;
490 12 100 100     85 if ((not defined $disk_id and $convert) or (defined $disk_id and not $convert)) {
      100        
      100        
491 4         13 $retrieved_disk_id = petscii_to_ascii($retrieved_disk_id);
492             }
493 12         154 return $retrieved_disk_id;
494             }
495              
496             =head2 full_disk_id
497              
498             Get full disk ID:
499              
500             my $full_disk_id = $diskBAM->full_disk_id($to_ascii);
501              
502             The first input parameter indicates whether value returned should get converted to an ASCII string upon retrieval:
503              
504             =over
505              
506             =item *
507             A false value defaults to the original 5-bytes long PETSCII string padded with $A0
508              
509             =item *
510             A true value enforces conversion of the original data to an ASCII string
511              
512             =back
513              
514             Set full disk ID:
515              
516             $diskBAM->full_disk_id($to_petscii, $full_disk_id);
517              
518             The first input parameter indicates whether C<$full_disk_id> parameter should get converted to a PETSCII string before storing:
519              
520             =over
521              
522             =item *
523             A false value indicates that C<$full_disk_id> has already been converted to a 5-bytes long PETSCII string and padded with $A0
524              
525             =item *
526             A true value enforces conversion of the original data to a valid PETSCII string
527              
528             =back
529              
530             Make sure that you either provide a valid PETSCII stream of bytes or use this option to get your original ASCII string properly converted.
531              
532             The second input parameter provides the actual full disk ID to be written to the BAM sector data.
533              
534             =cut
535              
536             sub full_disk_id {
537 16     16 1 2116 my $self = shift;
538 16         23 my $convert = shift;
539 16         28 my $full_disk_id = shift;
540 16 100       34 if (defined $full_disk_id) {
541 10         22 $self->_set_text_data(q{Full disk ID}, $full_disk_id, 5, $convert);
542             }
543 16         31 my $retrieved_full_disk_id = join '', map { chr } @{$self}[FULL_DISK_ID .. FULL_DISK_ID + 4];
  80         136  
  16         32  
544             # Remove padded $A0 bytes at the end of a PETSCII string:
545 16         55 substr ($retrieved_full_disk_id, -1) = q{} while $retrieved_full_disk_id =~ m/\xa0$/;
546 16 100 100     140 if ((not defined $full_disk_id and $convert) or (defined $full_disk_id and not $convert)) {
      100        
      100        
547 12         30 $retrieved_full_disk_id =~ s/\xa0/\x20/g;
548 12         33 $retrieved_full_disk_id = petscii_to_ascii($retrieved_full_disk_id);
549             }
550 16         868 return $retrieved_full_disk_id;
551             }
552              
553             =head2 dos_type
554              
555             Get DOS type:
556              
557             my $dos_type = $diskBAM->dos_type($to_ascii);
558              
559             The first input parameter indicates whether value returned should get converted to an ASCII string upon retrieval:
560              
561             =over
562              
563             =item *
564             A false value defaults to the original 2-bytes long PETSCII string padded with $A0
565              
566             =item *
567             A true value enforces conversion of the original data to an ASCII string
568              
569             =back
570              
571             Set DOS type:
572              
573             $diskBAM->dos_type($to_petscii, $dos_type);
574              
575             The first input parameter indicates whether C<$dos_type> parameter should get converted to a PETSCII string before storing:
576              
577             =over
578              
579             =item *
580             A false value indicates that C<$dos_type> has already been converted to a 2-bytes long PETSCII string and padded with $A0
581              
582             =item *
583             A true value enforces conversion of the original data to a valid PETSCII string
584              
585             =back
586              
587             Make sure that you either provide a valid PETSCII stream of bytes or use this option to get your original ASCII string properly converted.
588              
589             The second input parameter provides the actual DOS type to be written to the BAM sector data.
590              
591             =cut
592              
593             sub dos_type {
594 6     6 1 1683 my $self = shift;
595 6         10 my $convert = shift;
596 6         11 my $dos_type = shift;
597 6 100       14 if (defined $dos_type) {
598 4         12 $self->_set_text_data(q{DOS type}, $dos_type, 2, $convert);
599             }
600 6         67 my $retrieved_dos_type = join '', map { chr } @{$self}[DOS_TYPE .. DOS_TYPE + 1];
  12         27  
  6         11  
601             # Remove padded $A0 bytes at the end of a PETSCII string:
602 6         29 substr ($retrieved_dos_type, -1) = q{} while $retrieved_dos_type =~ m/\xa0$/;
603 6 100 100     45 if ((not defined $dos_type and $convert) or (defined $dos_type and not $convert)) {
      100        
      100        
604 4         14 $retrieved_dos_type = petscii_to_ascii($retrieved_dos_type);
605             }
606 6         200 return $retrieved_dos_type;
607             }
608              
609             sub _set_text_data {
610 43     43   64 my $self = shift;
611 43         62 my $var_name = shift;
612 43         58 my $text_data = shift;
613 43         86 my $max_length = shift;
614 43         64 my $convert = shift;
615              
616 43         166 my $var_bam_indexes = {
617             q{Disk name} => DISK_NAME,
618             q{Disk ID} => DISK_ID,
619             q{Full disk ID} => FULL_DISK_ID,
620             q{DOS type} => DOS_TYPE,
621             };
622 43         90 my $var_bam_index = $var_bam_indexes->{$var_name};
623              
624 43 100       85 if ($convert) {
625             # Warn if original ASCII string is longer than $max_length characters:
626 24 100       66 if (length ($text_data) > $max_length) {
627 1         174 carp sprintf q{%s to be set contains %d bytes: "%s" (note that only first %d bytes will be used)}, $var_name, length ($text_data), $text_data, $max_length;
628 1         100 substr ($text_data, $max_length) = q{};
629             }
630             # Convert an ASCII string to a PETSCII string:
631 24         65 $text_data = ascii_to_petscii($text_data);
632             # Pad with $A0 when necessary:
633 24         2869 $text_data .= chr 0xa0 while length ($text_data) < $max_length;
634             }
635             else {
636             # Warn if original PETSCII string is longer than $max_length characters:
637 19 100       50 if (length ($text_data) > $max_length) {
638 4         20 carp sprintf q{%s to be set contains %d bytes: "%s" (note that only first %d bytes will be used)}, $var_name, length ($text_data), petscii_to_ascii ($text_data), $max_length;
639 4         984 substr ($text_data, $max_length) = q{};
640             }
641             # Warn if original PETSCII string is shorter than $max_length characters:
642 19 100       57 if (length ($text_data) < $max_length) {
643 5         17 carp sprintf q{%s to be set contains %d bytes: "%s" (note that it will be padded with $A0 bytes to get full %d bytes string)}, $var_name, length ($text_data), petscii_to_ascii ($text_data), $max_length;
644             # Pad with $A0 when necessary:
645 5         1988 $text_data .= chr 0xa0 while length ($text_data) < $max_length;
646             }
647             }
648 43         70 splice @{$self}, $var_bam_index, $max_length, map { ord } split //, $text_data;
  43         194  
  382         684  
649             }
650              
651             =head2 num_free_sectors
652              
653             Get the number of free sectors on an entire disk:
654              
655             my $num_free_sectors = $diskBAM->num_free_sectors('all');
656              
657             Get the number of free sectors on the specified track:
658              
659             my $num_free_sectors = $diskBAM->num_free_sectors($track);
660              
661             When successful the number of free sectors on that track will be returned.
662              
663             Returns an undefined value if invalid track number has been provided.
664              
665             =cut
666              
667             sub num_free_sectors {
668 219     219 1 1537 my $self = shift;
669 219         269 my $track = shift;
670 219 100 66     610 if (defined $track && $track eq 'all') {
671 6         16 my $directory_first_track = $self->directory_first_track();
672 6         9 my $num_free_sectors = 0;
673 6         24 for my $track (1 .. scalar @SECTORS_PER_TRACK) {
674 210 100       338 next if $track == $directory_first_track; # skip directory track
675 204         322 $num_free_sectors += $self->num_free_sectors($track);
676             }
677 6         26 return $num_free_sectors;
678             }
679 213 100       328 unless ($self->_validate_track_number($track)) {
680 1         78 carp sprintf qq{Unable to get the number of free sectors on that track};
681 1         52 return undef;
682             }
683 212         298 my $track_bam_index = $TRACK_BAM_ENTRIES[$track - 1];
684             # The first byte of track BAM is the number of free sectors on that track:
685 212         260 my $num_free_sectors = $self->[$track_bam_index];
686 212         351 return $num_free_sectors;
687             }
688              
689             sub _increase_num_free_sectors {
690 5     5   14 my $self = shift;
691 5         8 my $track = shift;
692 5         11 my $track_bam_index = $TRACK_BAM_ENTRIES[$track - 1];
693             # The first byte of track BAM is the number of free sectors on that track:
694 5         8 my $num_free_sectors = $self->[$track_bam_index];
695             # Get number of sectors per track storage:
696 5         9 my $max_sector = $SECTORS_PER_TRACK[$track - 1];
697 5 100       12 if ($num_free_sectors >= $max_sector) {
698 1         119 croak sprintf qq{Internal error! Unable to increase the number of free sectors on track %s to %d, because it consists of %d sectors only}, $track, $num_free_sectors + 1, $max_sector;
699             }
700 4         9 $self->[$track_bam_index] = ++$num_free_sectors;
701             }
702              
703             sub _decrease_num_free_sectors {
704 27     27   41 my $self = shift;
705 27         55 my $track = shift;
706 27         48 my $track_bam_index = $TRACK_BAM_ENTRIES[$track - 1];
707             # The first byte of track BAM is the number of free sectors on that track:
708 27         67 my $num_free_sectors = $self->[$track_bam_index];
709 27 50       58 if ($num_free_sectors <= 0) {
710 0         0 croak sprintf qq{Internal error! Unable to decrease the number of free sectors on track %s to %d, because it already contains %d free sectors}, $track, $num_free_sectors + 1;
711             }
712 27         51 $self->[$track_bam_index] = --$num_free_sectors;
713             }
714              
715             sub _validate_track_number {
716 253     253   304 my $self = shift;
717 253         349 my $track = shift;
718 253 100 100     697 if ($track < 1 or $track > 35) {
719 2         285 carp sprintf qq{Invalid track number specified: %d}, $track;
720 2         192 return 0;
721             }
722             else {
723 251         491 return 1;
724             }
725             }
726              
727             =head2 sector_used
728              
729             Check if the sector is used:
730              
731             my $is_sector_used = $diskBAM->sector_used($track, $sector);
732              
733             True value indicates that the sector is used, false value states that the sector is free.
734              
735             Set specific sector to allocated:
736              
737             $diskBAM->sector_used($track, $sector, 1);
738              
739             Remove allocation from sector:
740              
741             $diskBAM->sector_used($track, $sector, 0);
742              
743             =cut
744              
745             sub sector_used {
746 40     40 1 3225 my $self = shift;
747 40         56 my $track = shift;
748 40         50 my $sector = shift;
749 40         59 my $is_used = shift;
750              
751 40 100       93 unless ($self->_validate_sector_number($track, $sector)) {
752 2         155 carp sprintf qq{Unable to get sector allocation};
753 2         103 return undef;
754             }
755              
756 38         66 my $track_bam_index = $TRACK_BAM_ENTRIES[$track - 1];
757 38         67 my $sector_bam_offset = $SECTOR_BAM_OFFSETS[$sector];
758 38         61 my $sector_bam_bitmask = $SECTOR_BAM_BITMASK[$sector];
759              
760 38 100       76 if (defined $is_used) {
761 33         50 my $sector_bam_bitmap = $self->[$track_bam_index + $sector_bam_offset];
762 33         62 my $was_sector_used_before = not ($sector_bam_bitmap & $sector_bam_bitmask);
763 33 100       67 if ($is_used) {
764             # Warn on repeated sector allocation:
765 28 100       60 if ($was_sector_used_before) {
766 1         103 carp sprintf qq{Warning! Allocating sector %d on track %d, which is already in use}, $sector, $track;
767             }
768             # Decrease the number of free sectors:
769             else {
770 27         78 $self->_decrease_num_free_sectors($track);
771             }
772             # Set specific sector to allocated:
773 28         156 $self->[$track_bam_index + $sector_bam_offset] &= ($sector_bam_bitmask ^ 0xff);
774             }
775             else {
776             # Warn on repeated sector deallocation:
777 5 100       41 unless ($was_sector_used_before) {
778 1         85 carp sprintf qq{Warning! Deallocating sector %d on track %d, which has been free before}, $sector, $track;
779             }
780             # Increase the number of free sectors:
781             else {
782 4         16 $self->_increase_num_free_sectors($track);
783             }
784             # Remove allocation from sector:
785 5         92 $self->[$track_bam_index + $sector_bam_offset] |= $sector_bam_bitmask;
786             }
787             }
788              
789 38         59 my $sector_bam_bitmap = $self->[$track_bam_index + $sector_bam_offset];
790              
791 38 100       70 if ($sector_bam_bitmap & $sector_bam_bitmask) {
792 8         21 return 0;
793             }
794             else {
795 30         57 return 1;
796             }
797             }
798              
799             =head2 sector_free
800              
801             Check if the sector is free:
802              
803             my $is_sector_free = $diskBAM->sector_free($track, $sector);
804              
805             True value indicates that the sector is free, false value states that the sector is used.
806              
807             Set specific sector to deallocated:
808              
809             $diskBAM->sector_free($track, $sector, 1);
810              
811             Remove sector from the list of empty sectors:
812              
813             $diskBAM->sector_free($track, $sector, 0);
814              
815             =cut
816              
817             sub sector_free {
818 10     10 1 74 my $self = shift;
819 10         15 my $track = shift;
820 10         12 my $sector = shift;
821 10         14 my $is_free = shift;
822              
823 10 100       21 my $is_used = not $is_free if defined $is_free;
824              
825 10         21 my $is_sector_used = $self->sector_used($track, $sector, $is_used);
826              
827 10 100       20 if ($is_sector_used) {
828 8         18 return 0;
829             }
830             else {
831 2         3 return 1;
832             }
833             }
834              
835             sub _validate_sector_number {
836 40     40   49 my $self = shift;
837 40         49 my $track = shift;
838 40         79 my $sector = shift;
839 40 100       94 unless ($self->_validate_track_number($track)) {
840 1         4 return 0;
841             }
842             else {
843             # Get number of sectors per track storage:
844 39         72 my $max_sector = $SECTORS_PER_TRACK[$track - 1];
845 39 100 66     134 if ($sector < 0 or $sector > $max_sector - 1) {
846 1         98 carp sprintf qq{Invalid sector number specified: %d}, $sector;
847 1         80 return 0;
848             }
849             else {
850 38         88 return 1;
851             }
852             }
853             }
854              
855             =head2 print_out_bam_layout
856              
857             Write BAM layout textual representation to a file handle:
858              
859             $diskBAM->print_out_bam_layout($fh);
860              
861             C<$fh> is expected to be an opened file handle that BAM layout's textual representation may be written to.
862              
863             =cut
864              
865             sub print_out_bam_layout {
866 0     0 1 0 my $self = shift;
867 0         0 my $fh = shift;
868 0         0 print q{ };
869 0         0 for (my $col = 0x00; $col < 0x10; $col++) {
870 0         0 printf q{%02X }, $col;
871             }
872 0         0 print qq{\n} . q{ } . '-' x 47 . qq{\n};
873 0         0 for (my $row = 0x00; $row < 0x100; $row += 0x10) {
874 0         0 printf q{%02X: }, $row;
875 0         0 for (my $col = 0x00; $col < 0x10; $col++) {
876 0         0 my $val = $self->[$row + $col];
877 0         0 printf q{%02X }, $val;
878             }
879 0         0 for (my $col = 0x00; $col < 0x10; $col++) {
880 0         0 my $val = $self->[$row + $col];
881 0 0 0     0 if ($val >= 0x20 and $val <= 0x7f) {
882 0         0 $val = ord petscii_to_ascii chr $val;
883             }
884             else {
885 0         0 $val = ord '?';
886             }
887 0         0 printf q{%c}, $val;
888             }
889 0         0 printf qq{\n};
890             }
891             }
892              
893             =head2 print_out_disk_header
894              
895             Print out formatted disk header line to a file handle:
896              
897             $diskBAM->print_out_disk_header($fh, $as_petscii);
898              
899             C defaults to the standard output. C defaults to false (meaning that ASCII characters will be printed out by default).
900              
901             =cut
902              
903             sub print_out_disk_header {
904 4     4 1 286 my ($self, $fh, $as_petscii) = @_;
905              
906 4   33     15 $fh ||= *STDOUT;
907 4         29 $fh->binmode(':bytes');
908              
909 4         22 my $stdout = select $fh;
910              
911 4 100       10 if ($as_petscii) {
912             # Get disk name as a PETSCII string:
913 2         6 my $disk_name = $self->disk_name(0);
914 2         15 $disk_name .= chr 0x20 while length $disk_name < 16;
915 2         6 $disk_name =~ s/\xa0/\x20/g;
916             # Get full disk ID as a PETSCII string:
917 2         6 my $full_disk_id = $self->full_disk_id(0);
918 2         9 $full_disk_id =~ s/\xa0/\x20/g;
919             # Setup an empty default disk header:
920 2         4 my @disk_header;
921             # Populate disk header with bytes:
922 2         6 push @disk_header, chr 0x30; # 0
923 2         6 push @disk_header, chr 0x20; # _
924 2         3 push @disk_header, chr 0x12; # RVS ON
925 2         3 push @disk_header, chr 0x22; # "
926 2         12 push @disk_header, split //, $disk_name;
927 2         6 push @disk_header, chr 0x22; # "
928 2         3 push @disk_header, chr 0x20; # _
929 2         7 push @disk_header, split //, $full_disk_id;
930 2         5 push @disk_header, chr 0x92; # RVS OFF
931             # Print out disk name and full disk ID:
932 2         10 print @disk_header;
933             }
934             else {
935             # Get disk name converted to an ASCII string:
936 2         7 my $disk_name = $self->disk_name(1);
937             # Get full disk ID converted to an ASCII string:
938 2         8 my $full_disk_id = $self->full_disk_id(1);
939             # Print out disk name and full disk ID:
940 2         12 printf q{0 "%-16s" %s}, $disk_name, $full_disk_id;
941             }
942              
943 4         82 select $stdout;
944              
945 4         11 return;
946             }
947              
948             =head2 print_out_blocks_free
949              
950             Print out number of free blocks line to a file handle:
951              
952             $diskBAM->print_out_blocks_free($fh, $as_petscii);
953              
954             C defaults to the standard output. C defaults to false (meaning that ASCII characters will be printed out by default).
955              
956             =cut
957              
958             sub print_out_blocks_free {
959 4     4 1 192 my ($self, $fh, $as_petscii) = @_;
960              
961 4   33     13 $fh ||= *STDOUT;
962 4         29 $fh->binmode(':bytes');
963              
964 4         20 my $stdout = select $fh;
965              
966             # Get number of free sectors on an entire disk:
967 4         11 my $num_free_sectors = $self->num_free_sectors('all');
968 4         13 my $blocks_free = sprintf q{%d blocks free.}, $num_free_sectors;
969              
970             # Print out number of free blocks:
971 4 100       11 if ($as_petscii) {
972 2         7 print petscii_to_ascii $blocks_free;
973             }
974             else {
975 2         6 printf $blocks_free;
976             }
977              
978 4         420 select $stdout;
979              
980 4         12 return;
981             }
982              
983             =head1 BUGS
984              
985             There are no known bugs at the moment. Please report any bugs or feature requests.
986              
987             =head1 CAVEATS
988              
989             There are some variations of the BAM layout, these are however not covered (yet!) by this module:
990              
991             =over
992              
993             =item *
994             DOLPHIN DOS 40-track extended format (track 36-40 BAM entries)
995              
996             =item *
997             SPEED DOS 40-track extended format (track 36-40 BAM entries)
998              
999             =back
1000              
1001             The BAM entries for SPEED, DOLPHIN and ProLogic DOS use the same layout as standard BAM entries, hence should be relatively easy to get implemented. Extended versions of this package may appear or they might as well get supported through other modules by the means of inheritance.
1002              
1003             =head1 EXPORT
1004              
1005             None. No method is exported into the caller's namespace either by default or explicitly.
1006              
1007             =head1 AUTHOR
1008              
1009             Pawel Krol, Epawelkrol@cpan.orgE.
1010              
1011             =head1 VERSION
1012              
1013             Version 0.05 (2021-01-16)
1014              
1015             =head1 COPYRIGHT AND LICENSE
1016              
1017             Copyright 2011-2021 by Pawel Krol .
1018              
1019             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.
1020              
1021             PLEASE NOTE THAT IT COMES WITHOUT A WARRANTY OF ANY KIND!
1022              
1023             =cut
1024              
1025             1;