File Coverage

blib/lib/Archive/ByteBoozer.pm
Criterion Covered Total %
statement 122 124 98.3
branch 37 46 80.4
condition 27 34 79.4
subroutine 26 27 96.3
pod 1 1 100.0
total 213 232 91.8


line stmt bran cond sub pod time code
1             package Archive::ByteBoozer;
2              
3             =head1 NAME
4              
5             Archive::ByteBoozer - Perl interface to David Malmborg's "ByteBoozer", a data cruncher for Commodore files
6              
7             =head1 SYNOPSIS
8              
9             use Archive::ByteBoozer qw/crunch/;
10              
11             # Read file, crunch data, write crunched data into a file:
12             my $original = new IO::File "original.prg", "r";
13             my $crunched = new IO::File "crunched.prg", "w";
14             crunch(source => $original, target => $crunched);
15              
16             # Read scalar, crunch data, write crunched data into a scalar:
17             my @data = (0x00, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05);
18             my $original = join '', map { chr $_ } @data;
19             my $crunched = new IO::Scalar;
20             crunch(source => $original, target => $crunched);
21              
22             # Crunch data preceding it with the given initial address first:
23             my $initial_address = 0x2000;
24             crunch(source => $original, target => $crunched, precede_initial_address => $initial_address);
25              
26             # Crunch data replacing the first two bytes with the new initial address first:
27             my $initial_address = 0x4000;
28             crunch(source => $original, target => $crunched, replace_initial_address => $initial_address);
29              
30             # Attach decruncher with the given execute program address:
31             my $program_address = 0x0c00;
32             crunch(source => $original, target => $crunched, attach_decruncher => $program_address);
33              
34             # Relocate compressed data to the given start address:
35             my $start_address = 0x0800;
36             crunch(source => $original, target => $crunched, relocate_output => $start_address);
37              
38             # Relocate compressed data to the given end address:
39             my $end_address = 0x2800;
40             crunch(source => $original, target => $crunched, relocate_output_up_to => $end_address);
41              
42             # Enable verbose output while crunching data:
43             my $verbose = 1;
44             crunch(source => $original, target => $crunched, verbose => $verbose);
45              
46             =head1 DESCRIPTION
47              
48             David Malmborg's "ByteBoozer" is a data cruncher for Commodore files written in C. In Perl the following operations are implemented via C package:
49              
50             =over
51              
52             =item *
53             Reading data from any given C interface (including files, scalars, etc.)
54              
55             =item *
56             Packing data using the compression algorithm implemented via ByteBoozer
57              
58             =item *
59             Writing data into any given C interface (including files, scalars, etc.)
60              
61             =back
62              
63             =head1 METHODS
64              
65             =cut
66              
67 5     5   28169 use bytes;
  5         50  
  5         24  
68 5     5   134 use strict;
  5         8  
  5         107  
69 5     5   21 use warnings;
  5         13  
  5         143  
70              
71 5     5   21 use base qw( Exporter );
  5         8  
  5         914  
72             our %EXPORT_TAGS = ();
73             $EXPORT_TAGS{'crunch'} = [ qw(&crunch) ];
74             $EXPORT_TAGS{'all'} = [ @{$EXPORT_TAGS{'crunch'}} ];
75             our @EXPORT_OK = @{ $EXPORT_TAGS{'all'} };
76             our @EXPORT = qw();
77              
78             our $VERSION = '0.08';
79              
80 5     5   4889 use Data::Dumper;
  5         47374  
  5         345  
81 5     5   4061 use IO::Scalar;
  5         60765  
  5         237  
82 5     5   4240 use Params::Validate qw(:all);
  5         1438890  
  5         1481  
83 5     5   42 use Scalar::Util qw(looks_like_number refaddr);
  5         13  
  5         11719  
84              
85             require XSLoader;
86             XSLoader::load(__PACKAGE__, $VERSION);
87              
88             =head2 crunch
89              
90             In order to crunch the data, you are required to provide source and target C interfaces:
91              
92             my $original = new IO::File "original.prg", "r";
93             my $crunched = new IO::File "crunched.prg", "w";
94             crunch(source => $original, target => $crunched);
95              
96             Upon writing the data into the target C interface current position in the stream will be reset to the initial position acquired before the subroutine call, which enables immediate access to the compressed data without the necessity of seeking to the right position in the stream. The same comment applies to the source C interface.
97              
98             In addition to the source and target C interfaces, which are mandatory arguments to the C subroutine call, the following parameters are recognized:
99              
100             =head3 attach_decruncher
101              
102             C enables to attach decruncher procedure with the given program start address:
103              
104             my $program_address = 0x0c00;
105             crunch(source => $in, target => $out, attach_decruncher => $program_address);
106              
107             This will create an executable BASIC file that is by default loaded into the memory area beginning at $0801 (assuming no output relocation has been requested), and jumping directly to the given execute program address of $0c00 upon completion of the decrunching process.
108              
109             =head3 make_executable
110              
111             C is an alias for C:
112              
113             my $program_address = 0x3600;
114             crunch(source => $in, target => $out, make_executable => $program_address);
115              
116             =head3 relocate_output
117              
118             C is used for setting up a new start address of the compressed data (by default data is shifted and aligned to fill in the memory up to the address of $fff9):
119              
120             my $start_address = 0x0800;
121             crunch(source => $in, target => $out, relocate_output => $start_address);
122              
123             This will relocate compressed data to the given start address of $0800 by writing an appropriate information in the output stream.
124              
125             =head3 relocate_output_up_to
126              
127             C is used for setting up a new start address of the compressed data by shifting it up to the given end address (by default data is shifted and aligned to fill in the memory up to the address of $fff9, however you might want to align your data to a different end address - it is where this option becomes handy):
128              
129             my $end_address = 0x2800;
130             crunch(source => $original, target => $crunched, relocate_output_up_to => $end_address);
131              
132             This will relocate compressed data to some address below $2800 by writing an appropriate information in the output stream.
133              
134             In the above example the following assumptions are true: your source code or other used data begins at $2800 and you want to load your compressed soundtrack file somewhere between $1000 and $2800, however you may still want to execute your decrunching routine later. This will not work if you load your compressed file at $1000, because uncompressed data would begin to overwrite your loaded data shortly after you invoked decruncher routine, leading to data corruption and truly unpredictable results. What you want to do is to load your file at any address that will provide data safety. C parameter ensures that.
135              
136             C and C parameters are mutually exclusive. C always takes precedence over C.
137              
138             =head3 precede_initial_address
139              
140             C adds given initial address at the beginning of an input stream, so this option can be used for setting up start address on the target device upon decrunching compressed data. If you are targetting a raw stream of bytes without providing initial memory address within it, decruncher routine will not be able to properly determine the right memory address, where your data should get unpacked to. Therefore it is essential to precede your input stream with the initial address, telling ByteBoozer what the target address of the uncompressed data is going to be:
141              
142             my $initial_address = 0x2000;
143             crunch(source => $in, target => $out, precede_initial_address => $initial_address);
144              
145             Before crunching algorithm is applied, data will be here prepended with the given initial address of $2000 first. This given initial address should be understood as the start address of the unpacked data (this is exactly where your compressed data is going to be uncrunched to).
146              
147             Please note that this option and C are mutually exclusive. This option is expected to be applied to a raw stream of bytes, while the latter one is not supposed to handle raw byte streams.
148              
149             =head3 replace_initial_address
150              
151             C replaces the original initial address that is found at the beginning of an input stream with the new initial address first, even before the whole crunching process begins. This option is therefore used the same like C for setting up a start address on the target device upon decrunching compressed data, however the initial address is not preceding the data, so that length of data remains unchanged, only its first two bytes get altered. This will tell ByteBoozer what is the new target address of the uncompressed data:
152              
153             my $initial_address = 0x4000;
154             crunch(source => $in, target => $out, replace_initial_address => $initial_address);
155              
156             Before crunching algorithm is applied, first two data bytes will be here replaced with the given initial address of $4000 first. This given initial address should be understood as the start address of the unpacked data (this is exactly where your compressed data is going to be uncrunched to).
157              
158             Please note that this option and C are mutually exclusive. This option is expected to be applied to a regular stream of C64 file data, while the latter one is not supposed to handle regular C64 data files.
159              
160             =head3 verbose
161              
162             C indicates display of the compression result:
163              
164             my $verbose = 1;
165             crunch(source => $in, target => $out, verbose => $verbose);
166              
167             When set to C<1> a similar informative message will be written to the standard output: C.
168              
169             =cut
170              
171             sub _memory_address_bytes {
172 8     8   12 my ($memory_address) = @_;
173 8         23 my $memory_address_lo = chr int $memory_address % 0x100;
174 8         25 my $memory_address_hi = chr int $memory_address / 0x100;
175 8         28 return ($memory_address_lo, $memory_address_hi);
176             }
177              
178             sub _read_file {
179 21     21   34 my ($params) = @_;
180 21         50 my $source = $params->{source};
181 21 100       74 die "Error (P-1): source file IO::Handle is closed, aborting" unless $source->opened;
182 19 50       205 $source->binmode(':bytes') if $source->can('binmode');
183 19         74 my ($buffer, $data, $n, $total_size) = ('');
184 19         93 while (($n = $source->sysread($data, 1)) != 0) {
185 65627         818163 $buffer .= $data;
186 65627         165727 $total_size++;
187             }
188 19         392 $params->{_source_data} = $buffer;
189 19         68 return;
190             }
191              
192             sub _crunch_data {
193 18     18   26 my ($params) = @_;
194 18         119 my $source_data = $params->{_source_data};
195 18         29 my $source_size = length $source_data;
196 18         109 my $source_file = bb_source($source_data, $source_size);
197 18         38 my $start_address = $params->{_start_address};
198 18         2179069 my $target_file = bb_crunch($source_file, $start_address);
199 18 100       110 die "Error (P-2): packed file too large, aborting" unless defined $target_file;
200 17         61 my $crunched_data = bb_data($target_file);
201 17 50       44 die "Error (B-1): cannot read crunched data, aborting" unless defined $crunched_data;
202 17         36 $params->{_crunched_data} = $crunched_data;
203 17         59 bb_free($source_file, $target_file);
204 17         55 return;
205             }
206              
207             sub _write_file {
208 17     17   24 my ($params) = @_;
209 17         28 my $target = $params->{target};
210 17 100       55 die "Error (P-3): target file IO::Handle is closed, aborting" unless $target->opened;
211 15         106 my $crunched_data = $params->{_crunched_data};
212 15 50       89 $target->binmode(':bytes') if $target->can('binmode');
213 15         71 while (length $crunched_data > 0) {
214 430         743 my $byte = substr $crunched_data, 0, 1, '';
215 430 50       796 die "Error (P-4): cannot write undefined value, aborting" unless defined $byte;
216 430         1004 my $num_bytes = $target->syswrite($byte, 1);
217 430 50       6886 die "Error (B-2): cannot write output stream, aborting" if $num_bytes != 1;
218             }
219 15 50       49 unless (defined $target->flush) {
220 0         0 die "Error (B-3): cannot flush output stream, aborting";
221             }
222 15         75 return;
223             }
224              
225             sub _attach_decruncher {
226 18     18   31 my ($params) = @_;
227 18   50     112 my $start_address = $params->{attach_decruncher} || $params->{make_executable} || 0;
228 18         38 $params->{_start_address} = $start_address;
229 18         27 return;
230             }
231              
232             sub _precede_initial_address {
233 19     19   32 my ($params) = @_;
234 19         33 my $precede_initial_address = $params->{precede_initial_address};
235 19 100       64 return unless defined $precede_initial_address;
236 4         14 my @memory_address = _memory_address_bytes($precede_initial_address);
237 4         110 substr $params->{_source_data}, 0, 0, join '', @memory_address;
238 4         11 return;
239             }
240              
241             sub _get_address_to_relocate_output_up_to {
242 2     2   4 my ($params) = @_;
243 2         4 my $data_length = length ($params->{_crunched_data}) - 0x02;
244 2         2 my $relocate_output_up_to = $params->{relocate_output_up_to};
245 2         3 my $address_to_relocate_data = $relocate_output_up_to - $data_length;
246 2         4 return $address_to_relocate_data;
247             }
248              
249             sub _relocate_output {
250 17     17   31 my ($params) = @_;
251 17         27 my $relocate_output = $params->{relocate_output};
252 17         26 my $relocate_output_up_to = $params->{relocate_output_up_to};
253 17 100 100     98 return unless defined $relocate_output || defined $relocate_output_up_to;
254 3 100       11 my $address_to_relocate_data =
255             defined $relocate_output_up_to ? _get_address_to_relocate_output_up_to($params) : $relocate_output;
256 3         11 my @memory_address = _memory_address_bytes($address_to_relocate_data);
257 3         10 substr $params->{_crunched_data}, 0, 2, join '', @memory_address;
258 3         7 return;
259             }
260              
261             sub _replace_initial_address {
262 18     18   30 my ($params) = @_;
263 18         28 my $replace_initial_address = $params->{replace_initial_address};
264 18 100       56 return unless defined $replace_initial_address;
265 1         3 my @memory_address = _memory_address_bytes($replace_initial_address);
266 1         4 substr $params->{_source_data}, 0, 2, join '', @memory_address;
267 1         3 return;
268             }
269              
270             sub crunch {
271 39     39 1 236199 my $params = { @_ };
272             validate(
273             @_, {
274             source => { type => HANDLE, isa => 'IO::Handle', callbacks => {
275 31 100   31   574 is_not_the_same_as_target => sub { exists $_[1]->{target} && refaddr $_[0] != refaddr $_[1]->{target} },
276             } },
277             target => { type => HANDLE, isa => 'IO::Handle', callbacks => {
278 29 100   29   547 is_not_the_same_as_source => sub { exists $_[1]->{source} && refaddr $_[0] != refaddr $_[1]->{source} },
279             } },
280             attach_decruncher => { type => SCALAR, optional => 1, callbacks => {
281 4 100 100 4   100 is_valid_memory_address => sub { looks_like_number $_[0] && $_[0] >= 0x0000 && $_[0] <= 0xffff },
282             } },
283             make_executable => { type => SCALAR, optional => 1, callbacks => {
284 0 0 0 0   0 is_valid_memory_address => sub { looks_like_number $_[0] && $_[0] >= 0x0000 && $_[0] <= 0xffff },
285             } },
286             precede_initial_address => { type => SCALAR, optional => 1, callbacks => {
287 7 100 100 7   147 is_valid_memory_address => sub { looks_like_number $_[0] && $_[0] >= 0x0000 && $_[0] <= 0xffff },
288             } },
289             relocate_output => { type => SCALAR, optional => 1, callbacks => {
290 5 100 100 5   107 is_valid_memory_address => sub { looks_like_number $_[0] && $_[0] >= 0x0000 && $_[0] <= 0xffff },
291             } },
292             relocate_output_up_to => { type => SCALAR, optional => 1, callbacks => {
293 2 50 33 2   45 is_valid_memory_address => sub { looks_like_number $_[0] && $_[0] >= 0x0000 && $_[0] <= 0xffff },
294             } },
295             replace_initial_address => { type => SCALAR, optional => 1, callbacks => {
296 4 100 100 4   97 is_valid_memory_address => sub { looks_like_number $_[0] && $_[0] >= 0x0000 && $_[0] <= 0xffff },
297             } },
298 39         2132 verbose => { type => SCALAR, optional => 1, regex => qr/^\d+$/ },
299             }
300             );
301 21         551 my $pos = _seek_and_tell($params->{source}, $params->{target});
302 21         88 my $source_position = $pos->{source}->{get}->($params->{source});
303 21         153 my $target_position = $pos->{target}->{get}->($params->{target});
304 21         118 _read_file $params;
305 19         50 _precede_initial_address $params;
306 19 100       70 die "Error (I-3): no data to crunch in input stream, aborting" unless length $params->{_source_data} > 1;
307 18         49 _replace_initial_address $params;
308 18         44 _attach_decruncher $params;
309 18         47 _crunch_data $params;
310 17         45 _relocate_output $params;
311 17         44 _write_file $params;
312 15         56 $pos->{source}->{set}->($params->{source}, $source_position);
313 15         279 $pos->{target}->{set}->($params->{target}, $target_position);
314 15 100 66     206 if (defined $params->{verbose} && $params->{verbose} == 1) {
315 1         64 printf("[Archive::ByteBoozer] Compressed %u bytes into %u bytes.\n", length $params->{_source_data}, length $params->{_crunched_data});
316             }
317 15         84 return;
318             }
319              
320             sub _seek_and_tell {
321 21     21   81 my ($source, $target) = @_;
322 21   100     131 my $source_getpos = $source->can('getpos') || \&IO::Scalar::getpos;
323 21   100     100 my $source_setpos = $source->can('setpos') || \&IO::Scalar::setpos;
324 21   100     91 my $target_getpos = $target->can('getpos') || \&IO::Scalar::getpos;
325 21   100     90 my $target_setpos = $target->can('setpos') || \&IO::Scalar::setpos;
326             return {
327 21         124 'source' => {
328             'get' => $source_getpos,
329             'set' => $source_setpos,
330             },
331             'target' => {
332             'get' => $target_getpos,
333             'set' => $target_setpos,
334             }
335             };
336             }
337              
338             =head1 EXAMPLES
339              
340             Compress a PRG file named "part-1.prg", replace its start address with $2000 (this is where the data will be uncompressed to), move all packed bytes to $f000 (this will be written into loading address of the output file), and save crunched data into a PRG file name "part-1.crunched.prg":
341              
342             use Archive::ByteBoozer qw/crunch/;
343              
344             my $source = new IO::File "part-1.prg", "r";
345             my $target = new IO::File "part-1.crunched.prg", "w";
346             my $unpacking_address = 0x2000;
347             my $relocate_address = 0xf000;
348              
349             crunch(
350             source => $source,
351             target => $target,
352             replace_initial_address => $unpacking_address,
353             relocate_output => $relocate_address,
354             );
355              
356             =head1 BUGS
357              
358             There are no known bugs at the moment. Please report any bugs or feature requests.
359              
360             =head1 EXPORT
361              
362             C exports nothing by default.
363              
364             You are allowed to explicitly import the crunch subroutine into the caller's namespace either by specifying its name in the import list (C) or by using the module with the C<:crunch> tag.
365              
366             =head1 SEE ALSO
367              
368             L, L
369              
370             =head1 AUTHOR
371              
372             Pawel Krol, Edjgruby@gmail.comE.
373              
374             =head1 VERSION
375              
376             Version 0.08 (2016-01-09)
377              
378             =head1 COPYRIGHT AND LICENSE
379              
380             ByteBoozer cruncher/decruncher:
381              
382             Copyright (C) 2004-2006, 2008-2009, 2012 David Malmborg.
383              
384             Archive::ByteBoozer Perl interface:
385              
386             Copyright (C) 2012-2013, 2016 by Pawel Krol.
387              
388             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.
389              
390             PLEASE NOTE THAT IT COMES WITHOUT A WARRANTY OF ANY KIND!
391              
392             =cut
393              
394             1;