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   73409 use bytes;
  5         89  
  5         22  
68 5     5   139 use strict;
  5         8  
  5         83  
69 5     5   21 use warnings;
  5         9  
  5         122  
70              
71 5     5   23 use base qw( Exporter );
  5         9  
  5         956  
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.10';
79              
80 5     5   2708 use Data::Dumper;
  5         28608  
  5         369  
81 5     5   2417 use IO::Scalar;
  5         59156  
  5         233  
82 5     5   2405 use Params::Validate qw(:all);
  5         46711  
  5         900  
83 5     5   39 use Scalar::Util qw(looks_like_number refaddr);
  5         8  
  5         7568  
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   16 my ($memory_address) = @_;
173 8         28 my $memory_address_lo = chr int $memory_address % 0x100;
174 8         24 my $memory_address_hi = chr int $memory_address / 0x100;
175 8         25 return ($memory_address_lo, $memory_address_hi);
176             }
177              
178             sub _read_file {
179 19     19   60 my ($params) = @_;
180 19         44 my $source = $params->{source};
181 19 100       61 die "Error (P-1): source file IO::Handle is closed, aborting" unless $source->opened;
182 18 50       175 $source->binmode(':bytes') if $source->can('binmode');
183 18         70 my ($buffer, $data, $n, $total_size) = ('');
184 18         94 while (($n = $source->sysread($data, 1)) != 0) {
185 65620         847835 $buffer .= $data;
186 65620         99122 $total_size++;
187             }
188 18         330 $params->{_source_data} = $buffer;
189 18         34 return;
190             }
191              
192             sub _crunch_data {
193 17     17   27 my ($params) = @_;
194 17         101 my $source_data = $params->{_source_data};
195 17         28 my $source_size = length $source_data;
196 17         109 my $source_file = bb_source($source_data, $source_size);
197 17         32 my $start_address = $params->{_start_address};
198 17         989545 my $target_file = bb_crunch($source_file, $start_address);
199 17 100       97 die "Error (P-2): packed file too large, aborting" unless defined $target_file;
200 16         54 my $crunched_data = bb_data($target_file);
201 16 50       38 die "Error (B-1): cannot read crunched data, aborting" unless defined $crunched_data;
202 16         29 $params->{_crunched_data} = $crunched_data;
203 16         58 bb_free($source_file, $target_file);
204 16         55 return;
205             }
206              
207             sub _write_file {
208 16     16   26 my ($params) = @_;
209 16         92 my $target = $params->{target};
210 16 100       103 die "Error (P-3): target file IO::Handle is closed, aborting" unless $target->opened;
211 15         99 my $crunched_data = $params->{_crunched_data};
212 15 50       83 $target->binmode(':bytes') if $target->can('binmode');
213 15         62 while (length $crunched_data > 0) {
214 430         706 my $byte = substr $crunched_data, 0, 1, '';
215 430 50       698 die "Error (P-4): cannot write undefined value, aborting" unless defined $byte;
216 430         753 my $num_bytes = $target->syswrite($byte, 1);
217 430 50       7619 die "Error (B-2): cannot write output stream, aborting" if $num_bytes != 1;
218             }
219 15 50       65 unless (defined $target->flush) {
220 0         0 die "Error (B-3): cannot flush output stream, aborting";
221             }
222 15         66 return;
223             }
224              
225             sub _attach_decruncher {
226 17     17   29 my ($params) = @_;
227 17   50     94 my $start_address = $params->{attach_decruncher} || $params->{make_executable} || 0;
228 17         31 $params->{_start_address} = $start_address;
229 17         30 return;
230             }
231              
232             sub _precede_initial_address {
233 18     18   33 my ($params) = @_;
234 18         34 my $precede_initial_address = $params->{precede_initial_address};
235 18 100       62 return unless defined $precede_initial_address;
236 4         15 my @memory_address = _memory_address_bytes($precede_initial_address);
237 4         91 substr $params->{_source_data}, 0, 0, join '', @memory_address;
238 4         12 return;
239             }
240              
241             sub _get_address_to_relocate_output_up_to {
242 2     2   3 my ($params) = @_;
243 2         5 my $data_length = length ($params->{_crunched_data}) - 0x02;
244 2         3 my $relocate_output_up_to = $params->{relocate_output_up_to};
245 2         2 my $address_to_relocate_data = $relocate_output_up_to - $data_length;
246 2         3 return $address_to_relocate_data;
247             }
248              
249             sub _relocate_output {
250 16     16   29 my ($params) = @_;
251 16         27 my $relocate_output = $params->{relocate_output};
252 16         30 my $relocate_output_up_to = $params->{relocate_output_up_to};
253 16 100 100     69 return unless defined $relocate_output || defined $relocate_output_up_to;
254 3 100       9 my $address_to_relocate_data =
255             defined $relocate_output_up_to ? _get_address_to_relocate_output_up_to($params) : $relocate_output;
256 3         9 my @memory_address = _memory_address_bytes($address_to_relocate_data);
257 3         9 substr $params->{_crunched_data}, 0, 2, join '', @memory_address;
258 3         6 return;
259             }
260              
261             sub _replace_initial_address {
262 17     17   31 my ($params) = @_;
263 17         24 my $replace_initial_address = $params->{replace_initial_address};
264 17 100       40 return unless defined $replace_initial_address;
265 1         4 my @memory_address = _memory_address_bytes($replace_initial_address);
266 1         3 substr $params->{_source_data}, 0, 2, join '', @memory_address;
267 1         3 return;
268             }
269              
270             sub crunch {
271 37     37 1 210573 my $params = { @_ };
272             validate(
273             @_, {
274             source => { type => HANDLE, isa => 'IO::Handle', callbacks => {
275 25 100   25   384 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 26 100   26   366 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   97 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   118 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   83 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   27 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   71 is_valid_memory_address => sub { looks_like_number $_[0] && $_[0] >= 0x0000 && $_[0] <= 0xffff },
297             } },
298 37         1616 verbose => { type => SCALAR, optional => 1, regex => qr/^\d+$/ },
299             }
300             );
301 19         555 my $pos = _seek_and_tell($params->{source}, $params->{target});
302 19         73 my $source_position = $pos->{source}->{get}->($params->{source});
303 19         129 my $target_position = $pos->{target}->{get}->($params->{target});
304 19         113 _read_file $params;
305 18         59 _precede_initial_address $params;
306 18 100       58 die "Error (I-3): no data to crunch in input stream, aborting" unless length $params->{_source_data} > 1;
307 17         46 _replace_initial_address $params;
308 17         47 _attach_decruncher $params;
309 17         44 _crunch_data $params;
310 16         46 _relocate_output $params;
311 16         50 _write_file $params;
312 15         56 $pos->{source}->{set}->($params->{source}, $source_position);
313 15         337 $pos->{target}->{set}->($params->{target}, $target_position);
314 15 100 66     1238 if (defined $params->{verbose} && $params->{verbose} == 1) {
315 1         49 printf("[Archive::ByteBoozer] Compressed %u bytes into %u bytes.\n", length $params->{_source_data}, length $params->{_crunched_data});
316             }
317 15         73 return;
318             }
319              
320             sub _seek_and_tell {
321 19     19   55 my ($source, $target) = @_;
322 19   100     110 my $source_getpos = $source->can('getpos') || \&IO::Scalar::getpos;
323 19   100     134 my $source_setpos = $source->can('setpos') || \&IO::Scalar::setpos;
324 19   100     70 my $target_getpos = $target->can('getpos') || \&IO::Scalar::getpos;
325 19   100     59 my $target_setpos = $target->can('setpos') || \&IO::Scalar::setpos;
326             return {
327 19         96 '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.10 (2018-11-26)
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, 2018 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;