File Coverage

blib/lib/D64/Disk/BAM.pm
Criterion Covered Total %
statement 329 361 91.1
branch 83 92 90.2
condition 42 57 73.6
subroutine 35 38 92.1
pod 16 16 100.0
total 505 564 89.5


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