File Coverage

blib/lib/D64/File/PRG.pm
Criterion Covered Total %
statement 219 288 76.0
branch 48 112 42.8
condition 7 17 41.1
subroutine 24 28 85.7
pod 7 7 100.0
total 305 452 67.4


line stmt bran cond sub pod time code
1             package D64::File::PRG;
2            
3             =head1 NAME
4            
5             D64::File::PRG - Handling individual C64's PRG files
6            
7             =head1 SYNOPSIS
8            
9             use D64::File::PRG;
10            
11             my $prg = D64::File::PRG->new('FILE' => $file);
12             my $prg = D64::File::PRG->new('RAW_DATA' => \$data, 'LOADING_ADDRESS' => 0x0801);
13            
14             $prg->change_loading_address('LOADING_ADDRESS' => 0x6400);
15            
16             my $data = $prg->get_data();
17             my $data = $prg->get_data('FORMAT' => 'ASM', 'ROW_LENGTH' => 10);
18            
19             $prg->set_data('RAW_DATA' => \$data, 'LOADING_ADDRESS' => 0x1000);
20             $prg->set_data('RAW_DATA' => \$data);
21             $prg->set_file_data('FILE_DATA' => \$file_data);
22            
23             $prg->write_file('FILE' => $file);
24            
25             =head1 DESCRIPTION
26            
27             D64::File::PRG is a Perl module providing the set of methods for handling individual C64's PRG files. It enables an easy access to the raw contents of any PRG file, manipulation of its loading address, and transforming binary data into assembly code understood by tools like "Dreamass" or "Turbo Assembler".
28            
29             =head1 METHODS
30            
31             =cut
32            
33 2     2   73014 use bytes;
  2         21  
  2         9  
34 2     2   54 use strict;
  2         5  
  2         68  
35 2     2   12 use warnings;
  2         7  
  2         55  
36            
37 2     2   11 use Carp;
  2         4  
  2         178  
38 2     2   10 use Exporter;
  2         5  
  2         82  
39 2     2   2132 use IO::Scalar;
  2         40718  
  2         117  
40 2     2   58 use Scalar::Util qw(looks_like_number);
  2         5  
  2         8442  
41            
42             our $VERSION = '0.03';
43             our @ISA = qw(Exporter);
44             our @EXPORT = ();
45             our @EXPORT_OK = qw();
46             our %EXPORT_TAGS = (default => [qw()]);
47            
48             local $| = 1;
49            
50             =head2 new
51            
52             A new D64::File::PRG object instance is created either by providing the path to an existing file, which is then being read into a memory, or by providing a scalar reference to the raw binary data with an accompanying loading address. This is illustrated by the following examples:
53            
54             my $prg = D64::File::PRG->new('FILE' => $file);
55             my $prg = D64::File::PRG->new('RAW_DATA' => \$data, 'LOADING_ADDRESS' => 0x0801);
56            
57             Constructor will die upon one of the following conditions:
58            
59             1. File size is less than two bytes.
60             2. Loading address provided is outside of a 16-bit range.
61             3. Any character within a raw data is not a single byte.
62            
63             There is an additional optional boolean "VERBOSE" available (defaulting to 0), which indicates that the extensive debugging messages should be printed out to the standard output. By default module acts silently, reporting error messages only.
64            
65             =cut
66            
67             sub new {
68 15     15 1 15541 my $this = shift;
69 15   33     75 my $class = ref($this) || $this;
70 15         25 my $self = {};
71 15         245 bless $self, $class;
72 15         43 $self->_initialize(@_);
73 15         40 return $self;
74             }
75            
76             sub _initialize {
77 15     15   21 my $self = shift;
78 15         50 my $params = {@_};
79 15         27 my $verbose = $params->{'VERBOSE'}; # display diagnostic messages
80 15         460 my $file = $params->{'FILE'}; # get data from file
81 15         23 my $loading_address = $params->{'LOADING_ADDRESS'}; # externally provided loading address
82 15         22 my $raw_data_ref = $params->{'RAW_DATA'}; # externally provided raw data
83 15         258 my ($package) = (caller(0))[0];
84 15         210 $self->{'VERBOSE'} = $verbose;
85 15 50       35 $self->_verbose_message('MESSAGE' => "Initializing new \"${package}\" object instance", 'ERROR' => 0) if $verbose;
86             # When "FILE" parameter is defined, it takes the precedence over "LOAD/DATA" parameters:
87 15 50       32 if (defined $file) {
88 0         0 $self->read_file('FILE' => $file);
89             }
90             else {
91             # LOADING_ADDRESS: loading address (must be a valid value)
92 15 50       233 unless (defined $loading_address) {
93 0         0 $self->_verbose_message('MESSAGE' => "an undefined loading address has been provided to the constructor", 'ERROR' => 1);
94             }
95 15         43 $self->_get_loading_address_from_scalar('LOADING_ADDRESS' => $loading_address, 'VERBOSE' => $verbose);
96             # RAW_DATA: binary data provided as a raw data scalar
97 15         78 $self->_get_raw_contents_from_scalarref('RAW_DATA' => $raw_data_ref, 'VERBOSE' => $verbose);
98             }
99 15 50       36 $self->_verbose_message('MESSAGE' => "Returning new object instance upon successful init", 'ERROR' => 0) if $verbose;
100 15         38 return;
101             }
102            
103             sub _get_loading_address_from_scalar {
104 18     18   23 my $self = shift;
105 18         51 my $params = {@_};
106 18         30 my $loading_address = $params->{'LOADING_ADDRESS'}; # externally provided loading address
107 18         227 my $verbose = $params->{'VERBOSE'}; # display diagnostic messages
108 18 100       64 unless (looks_like_number $loading_address) {
109 1         3 $self->_verbose_message('MESSAGE' => "a non-numeric scalar value cannot be converted into loading address", 'ERROR' => 1);
110             }
111 17         68 my $loading_address_readable = uc sprintf "\$%04x", $loading_address;
112 17 50       49 $self->_verbose_message('MESSAGE' => "Validating value of provided loading address: ${loading_address_readable}", 'ERROR' => 0) if $verbose;
113 17 50 33     82 if ($loading_address < 0x0000 or $loading_address > 0xffff) {
114 0         0 $self->_verbose_message('MESSAGE' => "invalid loading address provided (${loading_address_readable})", 'ERROR' => 1);
115             }
116 17 50       138 $self->_verbose_message('MESSAGE' => "Received the correct file loading address: ${loading_address_readable}", 'ERROR' => 0) if $verbose;
117 17         50 $self->{'LOADING_ADDRESS'} = $loading_address;
118             }
119            
120             =head2 read_file
121            
122             While operating an existing D64::File::PRG object instance, there is no need to create a new one when you simply want to replace it with the contents of another file, that is if you only want to load a new data (however you need to create a new object instance if you want to provide raw data through a scalar reference - this limitation should be patched with the next release of this module). The example follows:
123            
124             $prg->read_file('FILE' => $file);
125            
126             =cut
127            
128             sub read_file {
129 0     0 1 0 my $self = shift;
130 0         0 my $params = {@_};
131 0         0 my $file = $params->{'FILE'}; # get data from file
132 0         0 my $verbose = $self->{'VERBOSE'}; # display diagnostic messages
133             # Verify if file exists:
134 0 0       0 unless (-e $file) {
135 0         0 $self->_verbose_message('MESSAGE' => "file \"${file}\" does not exist", 'ERROR' => 1);
136             }
137 0         0 $self->{'FILENAME'} = $file;
138             # Read data from file:
139 0 0       0 $self->_verbose_message('MESSAGE' => "Opening file \"${file}\" for reading", 'ERROR' => 0) if $verbose;
140 0 0       0 open my $fh, '<', $file or $self->_verbose_message('MESSAGE' => "could not open filehandle for \"${file}\" file", 'ERROR' => 1);
141 0         0 binmode $fh, ':bytes';
142 0         0 $self->_read_file('FILEHANDLE' => $fh, 'VERBOSE' => $verbose);
143 0 0       0 close $fh or $self->_verbose_message('MESSAGE' => "could not close opened filehandle for \"${file}\" file", 'ERROR' => 1);
144 0 0       0 $self->_verbose_message('MESSAGE' => "Closing file \"${file}\" upon successful read", 'ERROR' => 0) if $verbose;
145             }
146            
147             sub _read_file {
148 4     4   12 my ($self, %params) = @_;
149            
150 4         7 my $fh = $params{FILEHANDLE};
151 4         5 my $verbose = $params{VERBOSE};
152            
153 4         11 $self->_get_loading_address_from_file(FILEHANDLE => $fh, VERBOSE => $verbose);
154 2         8 $self->_get_raw_contents_from_file(FILEHANDLE => $fh, VERBOSE => $verbose);
155            
156 2         5 return;
157             }
158            
159             sub _get_raw_contents_from_scalarref {
160 20     20   27 my $self = shift;
161 20         50 my $params = {@_};
162 20         60 my $raw_data_ref = $params->{'RAW_DATA'}; # externally provided raw data
163 20         33 my $verbose = $params->{'VERBOSE'}; # display diagnostic messages
164 20 100       58 unless (ref $raw_data_ref eq 'SCALAR') {
165 1 50       5 my $raw_data_reftype = ref ($raw_data_ref) ? ( ref ($raw_data_ref) . ' reference' ) : 'SCALAR itself';
166 1         7 $self->_verbose_message('MESSAGE' => "raw data has to be a SCALAR reference (but is a ${raw_data_reftype})", 'ERROR' => 1);
167             }
168 19         35 $self->{'RAW_DATA'} = []; # empty all previously stored raw file contents
169 19         47 my ($bytes_count, $byte) = (0);
170 19 50       37 $self->_verbose_message('MESSAGE' => "Retrieving raw file contents from a SCALAR reference", 'ERROR' => 0) if $verbose;
171 19         29 my $raw_data_length = length ${$raw_data_ref};
  19         33  
172 19         49 while ($bytes_count < $raw_data_length) {
173 116         127 my $byte = substr ${$raw_data_ref}, $bytes_count, 1;
  116         198  
174 116         151 my $byte_value = ord $byte; # get byte numeric value
175 116 50 33     496 if ($byte_value < 0x00 or $byte_value > 0xff) {
176 0         0 my $byte_value_readable = sprintf "\$%02x", $byte_value;
177 0         0 $self->_verbose_message('MESSAGE' => "invalid byte value (${byte_value_readable}) in raw data at offset ${bytes_count}", 'ERROR' => 1);
178             }
179 116         123 push @{$self->{'RAW_DATA'}}, $byte_value;
  116         215  
180 116         248 $bytes_count++;
181             }
182 19 50       68 $self->_verbose_message('MESSAGE' => "Received ${bytes_count} bytes of the raw file contents", 'ERROR' => 0) if $verbose;
183             }
184            
185             sub _get_raw_contents_from_file {
186 2     2   3 my $self = shift;
187 2         5 my $params = {@_};
188 2         3 my $fh = $params->{'FILEHANDLE'}; # already opened filehandle
189 2         4 my $verbose = $params->{'VERBOSE'}; # display diagnostic messages
190 2         4 my $file = $self->{'FILENAME'}; # filename associated with "$fh" filehandle
191 2         9 my ($bytes_count, $byte) = (0);
192 2         6 $self->{'RAW_DATA'} = []; # empty all previously stored raw file contents
193 2 50       7 $self->_verbose_message('MESSAGE' => "Retrieving raw file contents from an opened filehandle", 'ERROR' => 0) if $verbose;
194 2         7 while ( sysread $fh, $byte, 1 ) {
195 9         113 $bytes_count++;
196 9         10 push @{$self->{'RAW_DATA'}}, ord $byte; # get byte numeric value
  9         38  
197             }
198 2 50       29 $self->_verbose_message('MESSAGE' => "Received ${bytes_count} bytes of the raw file contents", 'ERROR' => 0) if $verbose;
199             }
200            
201             sub _get_loading_address_from_file {
202 4     4   6 my $self = shift;
203 4         10 my $params = {@_};
204 4         8 my $fh = $params->{'FILEHANDLE'}; # already opened filehandle
205 4         7 my $verbose = $params->{'VERBOSE'}; # display diagnostic messages
206 4         5 my $file = $self->{'FILENAME'}; # filename associated with "$fh" filehandle
207 4         6 my ($load_addr_lo, $load_addr_hi, $bytes_count);
208 4 50       9 $self->_verbose_message('MESSAGE' => "Retrieving loading address from an opened filehandle", 'ERROR' => 0) if $verbose;
209 4         27 $bytes_count = sysread $fh, $load_addr_lo, 1;
210 4 50       72 my $filename = defined $file ? qq{"${file}"} : q{IO::Scalar};
211 4 100       10 if ($bytes_count != 1) {
212 1         6 $self->_verbose_message('MESSAGE' => "unexpected end of file while reading loading address from $filename filehandle", 'ERROR' => 1);
213             }
214 3         8 $bytes_count = sysread $fh, $load_addr_hi, 1;
215 3 100       37 if ($bytes_count != 1) {
216 1         5 $self->_verbose_message('MESSAGE' => "unexpected end of file while reading loading address from $filename filehandle", 'ERROR' => 1);
217             }
218 2         6 my $loading_address = ord ($load_addr_lo) + 0x100 * ord ($load_addr_hi);
219 2         8 my $loading_address_readable = uc sprintf "\$%04x", $loading_address;
220 2 50       5 $self->_verbose_message('MESSAGE' => "Received the correct file loading address: ${loading_address_readable}", 'ERROR' => 0) if $verbose;
221 2         6 $self->{'LOADING_ADDRESS'} = $loading_address;
222             }
223            
224             =head2 get_data
225            
226             All raw data can be accessed through this method. You might explicitly want to request the format of a data retrieved. By default the raw content is collected unless you otherwise specify to get an assembly formatted source code. In both cases a scalar value is returned. In the latter case you are able to provide an additional parameter indicating how many byte values will be returned on a single line (these are 8 bytes by default). Here are a couple of examples:
227            
228             my $raw_data = $prg->get_data('FORMAT' => 'RAW', 'LOAD_ADDR_INCL' => 0);
229             my $asm_data = $prg->get_data('FORMAT' => 'ASM', 'LOAD_ADDR_INCL' => 1, 'ROW_LENGTH' => 4);
230            
231             There is an additional optional boolean "LOAD_ADDR_INCL", which indicates if a loading address should be included in the output string. For raw contents it defaults to 0, while for assembly source code format it defaults to 1. This is reasonable, as you usually don't want loading address included in a raw data, but it becomes quite useful when compiling a source code.
232            
233             =cut
234            
235             sub get_data {
236 16     16 1 271 my $self = shift;
237 16         44 my $params = {@_};
238 16         29 my $verbose = $self->{'VERBOSE'};
239 16         28 my $format = $params->{'FORMAT'}; # format of data returned from this method (defaults to "RAW")
240 16 100       38 $format = 'RAW' unless defined $format;
241             # LOAD_ADDR_INCL: a boolean indicating if loading address should be included to output
242             # It defaults to 0 for RAW format, and to 1 for ASM format
243 16         24 my $loading_address_included = $params->{'LOAD_ADDR_INCL'};
244 16 100       35 if ($format eq 'RAW') {
    50          
245 14 50       37 $self->_verbose_message('MESSAGE' => "Getting raw data contents into a scalar value", 'ERROR' => 0) if $verbose;
246 14         19 my $data = ''; # prepare scalar value with the whole RAW contents
247 14 100       55 $self->_add_loading_address_to_scalarref('RAW_DATA' => \$data, 'VERBOSE' => $verbose) if $loading_address_included;
248 14         38 $self->_add_raw_data_to_scalarref('RAW_DATA' => \$data, 'VERBOSE' => $verbose);
249 14         66 return $data;
250             } elsif ($format eq 'ASM') {
251 2 50       6 $loading_address_included = 1 unless defined $loading_address_included;
252 2 50       5 $self->_verbose_message('MESSAGE' => "Getting raw data composed as an assembly source code", 'ERROR' => 0) if $verbose;
253 2   100     9 my $row_length = $params->{'ROW_LENGTH'} || 8;
254 2 50 33     28 if ($row_length !~ m/^\d+$/ or $row_length < 0x01 or $row_length > 0xff) {
      33        
255 0         0 $self->_verbose_message('MESSAGE' => "invalid row length (\"${row_length}\") request for assembly raw data composition", 'ERROR' => 1);
256             }
257 2         4 my $data = ''; # prepare scalar value with the whole ASSEMBLY contents
258 2 50       7 if ($loading_address_included) {
259 2         9 $self->_compose_comment_line_to_scalarref('RAW_DATA' => \$data, 'ROW_LENGTH' => $row_length, 'VERBOSE' => $verbose);
260 2         8 $self->_compose_loading_address_to_scalarref('RAW_DATA' => \$data, 'VERBOSE' => $verbose);
261             }
262 2         7 $self->_compose_comment_line_to_scalarref('RAW_DATA' => \$data, 'ROW_LENGTH' => $row_length, 'VERBOSE' => $verbose);
263 2         7 $self->_compose_raw_data_to_scalarref('RAW_DATA' => \$data, 'ROW_LENGTH' => $row_length, 'VERBOSE' => $verbose);
264 2         6 $self->_compose_comment_line_to_scalarref('RAW_DATA' => \$data, 'ROW_LENGTH' => $row_length, 'VERBOSE' => $verbose);
265 2         8 return $data;
266             }
267             else {
268 0         0 $self->_verbose_message('MESSAGE' => "unrecognized data format (\"${format}\")", 'ERROR' => 1);
269             }
270             }
271            
272             sub _compose_comment_line_to_scalarref {
273 6     6   7 my $self = shift;
274 6         18 my $params = {@_};
275 6         11 my $data_ref = $params->{'RAW_DATA'}; # scalar value with the whole RAW contents
276 6         9 my $row_length = $params->{'ROW_LENGTH'}; # number of byte values per single line
277 6         9 my $verbose = $params->{'VERBOSE'}; # display diagnostic messages
278 6         7 ${$data_ref} .= ';' . '-' x (19 + 5 * $row_length) . "\n";
  6         33  
279             }
280            
281             sub _compose_raw_data_to_scalarref {
282 2     2   4 my $self = shift;
283 2         5 my $params = {@_};
284 2         3 my $data_ref = $params->{'RAW_DATA'}; # scalar value with the whole RAW contents
285 2         3 my $row_length = $params->{'ROW_LENGTH'}; # number of byte values per single line
286 2         4 my $verbose = $params->{'VERBOSE'}; # display diagnostic messages
287 2         2 my $line;
288 2         3 my $offset = 0;
289 2         2 foreach my $byte (@{$self->{'RAW_DATA'}}) {
  2         6  
290 6 100       25 if ($offset % $row_length == 0) {
291 3 100       9 if ($offset != 0) {
292 1         5 $line =~ s/, $//;
293 1         2 ${$data_ref} .= "${line}\n";
  1         4  
294             }
295 3         5 $line = ' ' x 16 . '.byte ';
296             }
297 6         12 my $byte_value = sprintf "\$%02x, ", $byte;
298 6         7 $line .= $byte_value;
299 6         12 $offset++;
300             }
301 2 50       10 ${$data_ref} .= "${line}\n" if $line =~ m/, $/i;
  2         6  
302 2         3 ${$data_ref} =~ s/, $//;
  2         13  
303             }
304            
305             sub _add_raw_data_to_scalarref {
306 14     14   20 my $self = shift;
307 14         37 my $params = {@_};
308 14         22 my $data_ref = $params->{'RAW_DATA'}; # scalar value with the whole RAW contents
309 14         22 my $verbose = $params->{'VERBOSE'}; # display diagnostic messages
310 14         17 foreach my $byte (@{$self->{'RAW_DATA'}}) {
  14         34  
311 79         108 my $raw_byte = chr $byte;
312 79         85 ${$data_ref} .= $raw_byte;
  79         156  
313             }
314             }
315            
316             sub _compose_loading_address_to_scalarref {
317 2     2   3 my $self = shift;
318 2         6 my $params = {@_};
319 2         4 my $data_ref = $params->{'RAW_DATA'}; # scalar value with the whole RAW contents
320 2         3 my $verbose = $params->{'VERBOSE'}; # display diagnostic messages
321 2         9 my $loading_address = sprintf "%04x", hex $self->{'LOADING_ADDRESS'};
322 2         3 ${$data_ref} .= ' ' x 16;
  2         6  
323 2         3 ${$data_ref} .= sprintf "*= \$%04x\n", $loading_address;
  2         10  
324             }
325            
326             sub _add_loading_address_to_scalarref {
327 9     9   14 my $self = shift;
328 9         23 my $params = {@_};
329 9         18 my $data_ref = $params->{'RAW_DATA'}; # scalar value with the whole RAW contents
330 9         12 my $verbose = $params->{'VERBOSE'}; # display diagnostic messages
331 9         29 my $loading_address = sprintf "%04x", $self->{'LOADING_ADDRESS'};
332 9         52 my ($addr_hi, $addr_lo) = ( $loading_address =~ m/([0-9a-f]{2})/ig );
333 9         14 ${$data_ref} .= chr (hex $addr_lo);
  9         24  
334 9         10 ${$data_ref} .= chr (hex $addr_hi);
  9         31  
335             }
336            
337             =head2 change_loading_address
338            
339             You can modify original file loading address by performing the following operation:
340            
341             $prg->change_loading_address('LOADING_ADDRESS' => 0x6400);
342            
343             =cut
344            
345             sub change_loading_address {
346 4     4 1 9 my $self = shift;
347 4         13 my $params = {@_};
348 4         8 my $loading_address = $params->{'LOADING_ADDRESS'}; # new loading address
349 4         9 my $verbose = $self->{'VERBOSE'}; # display diagnostic messages
350             # Verify if provided loading address is correct:
351 4 100       14 unless (defined $loading_address) {
352 1         6 $self->_verbose_message('MESSAGE' => "an undefined loading address has been provided to the method that was supposed to change its value", 'ERROR' => 1);
353             }
354             # Update loading address if correct value provided:
355 3         19 $self->_get_loading_address_from_scalar('LOADING_ADDRESS' => $loading_address, 'VERBOSE' => $verbose);
356 2 50       10 $self->_verbose_message('MESSAGE' => "File loading address has been succesfully updated", 'ERROR' => 0) if $verbose;
357             }
358            
359             =head2 set_data
360            
361             You can update raw program data and its loading address by performing the following operation:
362            
363             $prg->set_data('RAW_DATA' => \$data, 'LOADING_ADDRESS' => 0x1000);
364            
365             You can update raw program data without modifying its loading address by performing the following operation:
366            
367             $prg->set_data('RAW_DATA' => \$data);
368            
369             =cut
370            
371             sub set_data {
372 5     5 1 176 my ($self, %params) = @_;
373            
374 5         17 $self->_get_raw_contents_from_scalarref(%params);
375 4 100       20 $self->change_loading_address(%params) if exists $params{LOADING_ADDRESS};
376            
377 2         6 return;
378             }
379            
380             =head2 set_file_data
381            
382             You can replace original program data assuming that its loading address is included within the first two bytes of provided file data by performing the following operation:
383            
384             $prg->set_file_data('FILE_DATA' => \$file_data);
385            
386             =cut
387            
388             sub set_file_data {
389 4     4 1 103 my ($self, %params) = @_;
390            
391 4         7 my $file_data = $params{FILE_DATA};
392 4         6 my $verbose = $params{VERBOSE};
393            
394 4         26 my $fh = new IO::Scalar $file_data;
395 4         284 $self->_read_file(FILEHANDLE => $fh, VERBOSE => $verbose);
396 2         8 $fh->close;
397            
398 2         16 return;
399             }
400            
401             =head2 write_file
402            
403             There is a command allowing you to save the whole contents into a disk file:
404            
405             $prg->write_file('FILE' => $file, 'OVERWRITE' => 1);
406            
407             Note that when you specify any value evaluating to true for 'OVERWRITE' parameter, any existing file will be replaced (overwriting is disabled by default).
408            
409             =cut
410            
411             sub write_file {
412 0     0 1 0 my $self = shift;
413 0         0 my $params = {@_};
414 0         0 my $file = $params->{'FILE'}; # write data to file
415 0         0 my $overwrite = $params->{'OVERWRITE'}; # a boolean indicating if any existing file should be overwritten (no files will be overwritten by default)
416 0         0 my $verbose = $self->{'VERBOSE'}; # display diagnostic messages
417 0         0 $self->{'TARGET_FILENAME'} = $file;
418             # Write data to file:
419 0 0       0 $self->_verbose_message('MESSAGE' => "Opening file \"${file}\" for writing", 'ERROR' => 0) if $verbose;
420             # Verify if file exists:
421 0 0       0 if (-e $file) {
422 0 0       0 unless ($overwrite) {
423 0         0 $self->_verbose_message('MESSAGE' => "file \"${file}\" already exists", 'ERROR' => 1);
424             }
425             else {
426 0 0       0 $self->_verbose_message('MESSAGE' => "File exists (overwriting the existing content)", 'ERROR' => 0) if $verbose;
427             }
428             }
429 0 0       0 open my $fh, '>', $file or $self->_verbose_message('MESSAGE' => "could not open filehandle for \"${file}\" file", 'ERROR' => 1);
430 0         0 binmode $fh, ':bytes';
431 0         0 $self->_write_loading_address_to_file('FILEHANDLE' => $fh, 'VERBOSE' => $verbose);
432 0         0 $self->_write_raw_contents_to_file('FILEHANDLE' => $fh, 'VERBOSE' => $verbose);
433 0 0       0 close $fh or $self->_verbose_message('MESSAGE' => "could not close opened filehandle for \"${file}\" file", 'ERROR' => 1);
434 0 0       0 $self->_verbose_message('MESSAGE' => "Closing file \"${file}\" upon successful write", 'ERROR' => 0) if $verbose;
435             }
436            
437             sub _write_raw_contents_to_file {
438 0     0   0 my $self = shift;
439 0         0 my $params = {@_};
440 0         0 my $fh = $params->{'FILEHANDLE'}; # already opened filehandle
441 0         0 my $verbose = $params->{'VERBOSE'}; # display diagnostic messages
442 0         0 my $file = $self->{'TARGET_FILENAME'}; # filename associated with "$fh" filehandle
443 0         0 my ($bytes_count, $byte) = (0);
444 0 0       0 $self->_verbose_message('MESSAGE' => "Writing raw file contents into an opened filehandle", 'ERROR' => 0) if $verbose;
445 0         0 foreach my $byte (@{$self->{'RAW_DATA'}}) {
  0         0  
446 0         0 my $bytes_written = syswrite $fh, chr (hex $byte), 1;
447 0 0       0 if ($bytes_written != 1) {
448 0         0 $self->_verbose_message('MESSAGE' => "unexpected difficulties writing raw file contents to \"${file}\" filehandle at offset ${bytes_count} ($!)", 'ERROR' => 1);
449             }
450 0         0 $bytes_count++;
451             }
452 0 0       0 $self->_verbose_message('MESSAGE' => "Written ${bytes_count} bytes of the raw file contents", 'ERROR' => 0) if $verbose;
453             }
454            
455             sub _write_loading_address_to_file {
456 0     0   0 my $self = shift;
457 0         0 my $params = {@_};
458 0         0 my $fh = $params->{'FILEHANDLE'}; # already opened filehandle
459 0         0 my $verbose = $params->{'VERBOSE'}; # display diagnostic messages
460 0         0 my $file = $self->{'TARGET_FILENAME'}; # filename associated with "$fh" filehandle
461 0         0 my $loading_address = sprintf "%04x", $self->{'LOADING_ADDRESS'};
462 0         0 my ($addr_hi, $addr_lo) = ( $loading_address =~ m/([0-9a-f]{2})/ig );
463 0         0 my $bytes_count;
464 0 0       0 $self->_verbose_message('MESSAGE' => "Writing loading address into an opened filehandle", 'ERROR' => 0) if $verbose;
465 0         0 $bytes_count = syswrite $fh, chr (hex $addr_lo), 1;
466 0 0       0 if ($bytes_count != 1) {
467 0         0 $self->_verbose_message('MESSAGE' => "unexpected difficulties writing loading address to \"${file}\" filehandle ($!)", 'ERROR' => 1);
468             }
469 0         0 $bytes_count = syswrite $fh, chr (hex $addr_hi), 1;
470 0 0       0 if ($bytes_count != 1) {
471 0         0 $self->_verbose_message('MESSAGE' => "unexpected difficulties writing loading address to \"${file}\" filehandle ($!)", 'ERROR' => 1);
472             }
473 0         0 my $loading_address_readable = uc sprintf "\$%04x", $self->{'LOADING_ADDRESS'};
474 0 0       0 $self->_verbose_message('MESSAGE' => "Written the following file loading address: ${loading_address_readable}", 'ERROR' => 0) if $verbose;
475             }
476            
477             sub _verbose_message {
478 5     5   9 my $self = shift;
479 5         13 my $params = {@_};
480 5         10 my $message = $params->{'MESSAGE'};
481 5         16 my $error = $params->{'ERROR'};
482 5         17 my ($package, $line, $subroutine) = (caller(1))[0,2,3];
483 5 50       234 ($package, $line, $subroutine) = (caller(0))[0,2,3] if $package eq 'main';
484 5 50       12 if ($error) {
485 5         76 croak "[${package}][ERROR] ${subroutine} subroutine error at line ${line}: ${message}";
486             }
487             else {
488 0           print "[${package}][INFO] ${message}\n";
489             }
490             }
491            
492             =head1 EXAMPLES
493            
494             Retrieving raw data as an assembly formatted source code can be expressed using the following few lines of Perl code:
495            
496             use D64::File::PRG;
497             my $data = join ('', map {chr} (1,2,3,4,5));
498             my $prg = D64::File::PRG->new('RAW_DATA' => \$data, 'LOADING_ADDRESS' => 0x0801);
499             my $src = $prg->get_data('FORMAT' => 'ASM', 'ROW_LENGTH' => 4);
500             print $src;
501            
502             When executed, it prints out the source code that is ready for compilation:
503            
504             ;---------------------------------------
505             *= $0801
506             ;---------------------------------------
507             .byte $01, $02, $03, $04
508             .byte $05
509             ;---------------------------------------
510            
511             =head1 BUGS
512            
513             There are no known bugs at the moment. Please report any bugs or feature requests.
514            
515             =head1 EXPORT
516            
517             None. No method is exported into the caller's namespace either by default or explicitly.
518            
519             =head1 SEE ALSO
520            
521             I am working on the set of modules providing an easy way to access and manipulate the contents of D64 disk images and T64 tape images. D64::File::PRG is the first module of this set, as it provides operations necessary for handling individual C64's PRG files, which are the smallest building blocks for those images. Upon completion I am going to successively upload all my new modules into the CPAN.
522            
523             =head1 AUTHOR
524            
525             Pawel Krol, Epawelkrol@cpan.orgE.
526            
527             =head1 VERSION
528            
529             Version 0.03 (2013-01-19)
530            
531             =head1 COPYRIGHT AND LICENSE
532            
533             Copyright (C) 2010, 2013 by Pawel Krol.
534            
535             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.
536            
537             PLEASE NOTE THAT IT COMES WITHOUT A WARRANTY OF ANY KIND!
538            
539             =cut
540            
541             1;