File Coverage

lib/SIRTX/VM/Chunk.pm
Criterion Covered Total %
statement 26 198 13.1
branch 0 140 0.0
condition 0 18 0.0
subroutine 9 21 42.8
pod 10 10 100.0
total 45 387 11.6


line stmt bran cond sub pod time code
1             # Copyright (c) 2025 Philipp Schafft
2              
3             # licensed under Artistic License 2.0 (see LICENSE file)
4              
5             # ABSTRACT: module for interacting with SIRTX VM chunks
6              
7              
8             package SIRTX::VM::Chunk;
9              
10 1     1   314699 use v5.16;
  1         5  
11 1     1   8 use strict;
  1         2  
  1         40  
12 1     1   5 use warnings;
  1         2  
  1         62  
13              
14 1     1   6 use Carp;
  1         2  
  1         116  
15 1     1   7 use Fcntl qw(SEEK_SET SEEK_CUR SEEK_END);
  1         2  
  1         79  
16              
17 1     1   1002 use Data::Identifier;
  1         191174  
  1         8  
18 1     1   1050 use Data::Identifier::Util v0.24;
  1         6023  
  1         60  
19              
20 1     1   9 use parent qw(Data::Identifier::Interface::Userdata Data::Identifier::Interface::Known);
  1         5  
  1         6  
21              
22             our $VERSION = v0.03;
23              
24             use constant {
25 1         5 WK_SID => Data::Identifier->new(uuid => 'f87a38cb-fd13-4e15-866c-e49901adbec5'), # small-identifier
26             WK_SNI => Data::Identifier->new(uuid => '039e0bb7-5dd3-40ee-a98c-596ff6cce405'), # sirtx-numerical-identifier
27             WK_HDI => Data::Identifier->new(uuid => 'f8eb04ef-3b8a-402c-ad7c-1e6814cb1998'), # host-defined-identifier
28             WK_UDI => Data::Identifier->new(uuid => '05af99f9-4578-4b79-aabe-946d8e6f5888'), # user-defined-identifier
29              
30             FLAG_STANDALONE => (1<<7),
31             FLAG_CHUNK_IDENTIFIER => (1<<1),
32             FLAG_PADDING => (1<<0),
33 1     1   179 };
  1         3  
34              
35             my $_util = Data::Identifier::Util->new;
36              
37             my %_flags = map {$_ => 1} qw(standalone);
38              
39             # Fields:
40             # - Opcode
41             # - Extra
42             # * Flags
43             # * Type
44             # * Chunk identifier
45             # - Data
46             # * Padding
47              
48             my %_type_to_pkg;
49             my %_pkg_to_type;
50              
51             {
52             my %_sni_to_pkg = (
53             197 => 'OctetStream',
54             230 => 'ColourPalette',
55             237 => 'Padding',
56             );
57              
58             foreach my $sni (keys %_sni_to_pkg) {
59             my $type = Data::Identifier->new(sni => $sni)->register;
60             my $pkg = 'SIRTX::VM::Chunk::Type::'.$_sni_to_pkg{$sni};
61              
62             $_type_to_pkg{$_util->render_sirtx($type)} = $pkg;
63             $_pkg_to_type{$pkg} = $type;
64             }
65             }
66              
67              
68             sub new {
69 0     0 1   my ($pkg, %opts) = @_;
70 0           my $self = bless {
71             standalone => undef,
72             type => undef,
73             chunk_identifier => undef,
74             data => undef,
75             }, $pkg;
76              
77 0 0         if (defined(my $from = delete $opts{from})) {
78 0 0         if ($from->isa('File::Information::Inode')) {
79 0   0       $opts{type} //= $from->get('mediatype', as => 'Data::Identifier');
80 0           $self->attach_data($from->open_handle);
81             } else {
82 0           croak 'Not a supported type';
83             }
84             }
85              
86 0 0         if (defined(my $v = delete $opts{type})) {
87 0           $self->type($v);
88             }
89              
90 0 0         if (defined(my $v = delete $opts{flags})) {
91 0           foreach my $flag (keys %{$v}) {
  0            
92 0           $self->set_flag($flag => $v->{$flag});
93             }
94             }
95              
96 0 0         croak 'Stray options passed' if scalar keys %opts;
97              
98 0           return $self;
99             }
100              
101              
102             sub write {
103 0     0 1   my ($self, $out, @opts) = @_;
104 0           my $chunk_identifier = $self->chunk_identifier;
105 0           my $data_length = $self->_data_length;
106 0 0         my $body_length = 4 + (defined $chunk_identifier ? 2 : 0) + $data_length + ($data_length & 1);
107 0           my $body_length_extra = $body_length / 2;
108 0           my $flags = 0;
109 0           my $extra;
110             my $type;
111              
112 0 0         croak 'Stray options passed' if scalar @opts;
113              
114 0 0         croak 'Output handle in bad state: not 16 bit aligned' if $out->tell & 1;
115              
116 0 0         if ($body_length_extra <= 0xFFFF) {
    0          
117 0           $extra = pack('n', $body_length_extra);
118             } elsif ($body_length_extra <= 0xFFFF_FFFF) {
119 0           $extra = pack('N', $body_length_extra);
120             } else {
121 0           ...;
122             }
123              
124             {
125 0           my $type_id = $self->type;
  0            
126              
127 0           $type = eval {$type_id->as(WK_SNI)};
  0            
128 0 0         if (defined $type) {
129 0           $flags |= (0<<15)|(0<<14);
130 0           last;
131             }
132              
133 0           $type = eval {$type_id->as(WK_SID)};
  0            
134 0 0         if (defined $type) {
135 0           $flags |= (0<<15)|(1<<14);
136 0           last;
137             }
138              
139 0           $type = eval {$type_id->as(WK_HDI)};
  0            
140 0 0         if (defined $type) {
141 0           $flags |= (1<<15)|(0<<14);
142 0           last;
143             }
144              
145 0           $type = eval {$type_id->as(WK_UDI)};
  0            
146 0 0         if (defined $type) {
147 0           $flags |= (1<<15)|(1<<14);
148 0           last;
149             }
150              
151 0           croak 'Unsupported type';
152             }
153              
154 0 0         $flags |= FLAG_STANDALONE if $self->{standalone};
155 0 0         $flags |= FLAG_CHUNK_IDENTIFIER if defined $chunk_identifier;
156 0 0         $flags |= FLAG_PADDING if $data_length & 1;
157              
158 0           $out->print(pack('na*nn', 0x0638 + length($extra)/2, $extra, $flags, $type));
159              
160 0 0         if (defined $chunk_identifier) {
161 0           $out->print(pack('n', $chunk_identifier));
162             }
163              
164             {
165 0           my $todo = $data_length;
  0            
166 0           my $in = $self->{data}{fh};
167 0           my $restore = $in->tell;
168              
169 0 0         $in->seek($self->{data}{offset}, SEEK_SET) or croak 'Cannot seek to correct input position';
170              
171 0           while ($todo) {
172 0 0         my $step = $todo > 4096 ? 4096 : $todo;
173 0           my $got = read($in, my $data, $step);
174              
175 0 0 0       if (!defined($got) || $got < 1) {
176 0           last;
177             }
178              
179 0           $out->print($data);
180              
181 0           $todo -= $got;
182             }
183              
184 0 0         $in->seek($restore, SEEK_SET) or croak 'Cannot seek back on input';
185              
186 0 0         croak 'Incompelete data read on input' if $todo;
187             }
188              
189 0 0         $out->print(chr(0)) if $data_length & 1;
190             }
191              
192              
193             sub read {
194 0     0 1   my ($self, $in, @opts) = @_;
195 0           my $opcode;
196             my $data;
197 0           my $extra2;
198 0           my ($flags, $type);
199 0           my $chunk_identifier;
200 0           my $data_length;
201 0           my $data_offset;
202              
203 0 0         croak 'Stray options passed' if scalar @opts;
204              
205 0 0         $in->read($data, 2) == 2 or croak 'Cannot read opcode';
206 0           $opcode = unpack('n', $data);
207              
208 0 0         croak sprintf('Bad opcode: 0x%04x', $opcode) unless ($opcode & 0xFFF8) == 0x0638;
209              
210 0 0         if (($opcode & 0x7) == 1) {
    0          
211 0 0         $in->read($data, 2) == 2 or croak 'Cannot read extra';
212 0           $extra2 = unpack('n', $data) * 2;
213             } elsif (($opcode & 0x7) == 2) {
214 0 0         $in->read($data, 4) == 2 or croak 'Cannot read extra';
215 0           $extra2 = unpack('N', $data) * 2;
216             } else {
217             ...
218 0           }
219              
220 0 0         $in->read($data, 4) == 4 or croak 'Cannot read header';
221 0           ($flags, $type) = unpack('nn', $data);
222              
223 0 0         if ($flags & FLAG_CHUNK_IDENTIFIER) {
224 0 0         $in->read($data, 2) == 2 or croak 'Cannot read chunk identifier';
225 0           $chunk_identifier = unpack('n', $data);
226             }
227              
228 0 0         $data_length = $extra2 - 4 - ($flags & FLAG_CHUNK_IDENTIFIER ? 2 : 0) - ($flags & FLAG_PADDING ? 1 : 0);
    0          
229              
230 0 0         $data_offset = $in->tell or croak 'Cannot tell on input handle';
231              
232 0           $in->seek($data_length, SEEK_CUR);
233              
234 0 0         if ($flags & FLAG_PADDING) {
235 0 0         $in->read($data, 1) == 1 or croak 'Cannot read padding';
236 0 0         croak 'Invalid padding' unless ord($data) == 0;
237             }
238              
239 0           $self->attach_data($in, $data_offset, $data_length);
240              
241             {
242 0           my $flags_type = $flags & ((1<<15)|(1<<14));
  0            
243              
244 0 0         if ($flags_type == ((0<<15)|(0<<14))) {
    0          
    0          
    0          
245 0           $self->type(Data::Identifier->new(sni => $type));
246             } elsif ($flags_type == ((0<<15)|(1<<14))) {
247 0           $self->type(Data::Identifier->new(sid => $type));
248             } elsif ($flags_type == ((1<<15)|(0<<14))) {
249 0           $self->type(Data::Identifier->new(WK_HDI => $type));
250             } elsif ($flags_type == ((1<<15)|(1<<14))) {
251 0           $self->type(Data::Identifier->new(WK_UDI => $type));
252             }
253             }
254              
255 0           $self->set_flag(standalone => $flags & FLAG_STANDALONE);
256 0           $self->chunk_identifier($chunk_identifier);
257             }
258              
259              
260             sub type {
261 0     0 1   my ($self, $n, @opts) = @_;
262              
263 0 0         croak 'Stray options passed' if scalar @opts;
264              
265 0 0         if (defined $n) {
266 0           $self->{type} = Data::Identifier->new(from => $n);
267              
268 0 0         if (defined(my $pkg = $_type_to_pkg{$_util->render_sirtx($self->{type})})) {
269 0 0         if (ref($self) ne $pkg) {
270 0           require ($pkg =~ s/::/\//gr).'.pm';
271 0           $pkg->_upgrade($self);
272             }
273             }
274             }
275              
276 0           return $self->{type};
277             }
278              
279              
280             sub chunk_identifier {
281 0     0 1   my ($self, $n, @opts) = @_;
282              
283 0 0         croak 'Stray options passed' if scalar @opts;
284              
285 0 0         if (defined $n) {
286 0           $n =~ s/^~([0-9]+)$/$1/;
287 0           $n = int($n);
288              
289 0 0         croak 'Bad chunk identifier' unless $n >= 0;
290              
291 0 0         croak 'Chunk is read only' if $self->{read_only};
292              
293 0 0         $n = undef unless $n > 0;
294 0           $self->{chunk_identifier} = $n;
295             }
296              
297 0           return $self->{chunk_identifier};
298             }
299              
300              
301             sub padding {
302 0     0 1   my ($self, @opts) = @_;
303              
304 0 0         croak 'Stray options passed' if scalar @opts;
305              
306 0           return $self->_data_length & 1;
307             }
308              
309              
310             sub flag {
311 0     0 1   my ($self, $flag, $n, @opts) = @_;
312              
313 0 0         croak 'Stray options passed' if scalar @opts;
314              
315 0 0         croak 'Not a known flag: '.$flag unless $_flags{$flag};
316              
317 0 0         if (defined $n) {
318 0           $self->{$flag} = !!$n;
319             }
320              
321 0           return $self->{$flag};
322             }
323              
324              
325             sub set_flag {
326 0     0 1   my ($self, $flag, $n, @opts) = @_;
327              
328 0   0       $n //= 0;
329              
330 0           return $self->flag($flag, $n, @opts);
331             }
332              
333              
334             sub attach_data {
335 0     0 1   my ($self, $fh, $offset, $length, @opts) = @_;
336              
337 0 0         croak 'Stray options passed' if scalar @opts;
338              
339 0 0         croak 'No valid data handle given' unless ref $fh;
340              
341 0   0       $offset //= $fh->tell // croak 'Cannot tell position on data handle';
      0        
342              
343 0 0         unless (defined $length) {
344 0           my $pos = $fh->tell;
345              
346 0 0         $fh->seek(0, SEEK_END) or croak 'Cannot seek in data handle';
347              
348 0           $length = $fh->tell - $offset;
349              
350 0 0         $fh->seek($pos, SEEK_SET) or croak 'Cannot seek in data handle';
351             }
352              
353             $self->{data} = {
354 0           fh => $fh,
355             offset => $offset,
356             length => $length,
357             };
358             }
359              
360              
361             sub read_data {
362 0     0 1   my ($self, $length, $offset) = @_;
363 0           my $data = $self->{data};
364 0           my $res;
365              
366 0   0       $length = int($length // 0);
367 0   0       $offset = int($offset // 0);
368              
369 0 0         croak 'Bad length' if $length < 0;
370 0 0         croak 'Bad offset' if $offset < 0;
371 0 0         croak 'No data attached' unless defined $data;
372              
373 0 0         return undef if $offset >= $data->{length};
374              
375 0 0         $length = $data->{length} - $offset if ($length + $offset) > $data->{length};
376              
377             {
378 0           my $pos = $data->{fh}->tell;
  0            
379              
380 0 0         $data->{fh}->seek($data->{offset} + $offset, SEEK_SET) or croak 'Cannot seek forward';
381              
382 0           $data->{fh}->read($res, $length);
383              
384 0 0         $data->{fh}->seek($pos, SEEK_SET) or croak 'Cannot seek back';
385             }
386              
387 0           return $res;
388             }
389              
390             # ---- Private helpers ----
391              
392             sub _data_length {
393 0     0     my ($self) = @_;
394              
395 0 0         croak 'No data attached' unless defined $self->{data};
396              
397 0           return $self->{data}{length};
398             }
399              
400             sub _type {
401 0     0     my ($self) = @_;
402              
403 0           return $_pkg_to_type{ref $self};
404             }
405              
406             1;
407              
408             __END__
409              
410             =pod
411              
412             =encoding UTF-8
413              
414             =head1 NAME
415              
416             SIRTX::VM::Chunk - module for interacting with SIRTX VM chunks
417              
418             =head1 VERSION
419              
420             version v0.03
421              
422             =head1 SYNOPSIS
423              
424             use SIRTX::VM::Chunk;
425              
426             This package inherits from L<Data::Identifier::Interface::Userdata> and L<Data::Identifier::Interface::Known>.
427              
428             =head1 METHODS
429              
430             =head2 new
431              
432             my SIRTX::VM::Chunk $chunk = SIRTX::VM::Chunk->new;
433              
434             (since v0.01)
435              
436             Creates a new chunk object.
437              
438             The following options are supported:
439              
440             =over
441              
442             =item C<type>
443              
444             The type of the chunk as per L</type>.
445              
446             =item C<flags>
447              
448             A hashref with flags that are to be set as per L</set_flag>.
449              
450             =item C<from>
451              
452             (since v0.02)
453              
454             Takes values from the object passed.
455             Currently supported:
456             L<File::Information::Inode>.
457              
458             =back
459              
460             =head2 write
461              
462             $chunk->write($fh);
463              
464             (since v0.01)
465              
466             Writes the chunk to the given file handle.
467             No options are supported.
468              
469             =head2 read
470              
471             $chunk->read($fh);
472              
473             (since v0.01)
474              
475             Reads the chunk from the given file handle.
476             Resets all internal state of the chunk.
477             No options are supported.
478              
479             B<Note:>
480             The handle to read from needs to support L<perlfunc/tell> and L</perlfunc/seek>.
481              
482             B<Note:>
483             A reference to the handle is stored in the object as per L</attach_data>.
484             See details on what operations are allowed on the handle after this call.
485              
486             =head2 type
487              
488             my Data::Identifier $type = $chunk->type;
489             # or:
490             $chunk->type($type);
491              
492             (since v0.01)
493              
494             Gets or sets the type of the chunk.
495              
496             If the type is set and C<$type> is not a L<Data::Identifier> it is converted
497             as per L<Data::Identifier/new> using C<from>.
498              
499             B<Note:>
500             In order to be useable a type must have a valid I<sid> (C<small-identifier>),
501             I<sni> (C<sirtx-numerical-identifier>) assigned, or being mapped to a host defined identifier,
502             or a private identifier.
503              
504             B<Note:>
505             Once set to a supported value this value might become read only.
506             Hence one should only set it once.
507             That is via L</new>, being read in via L</read>, or manually using this method.
508              
509             =head2 chunk_identifier
510              
511             my $chunk_identifier = $chunk->chunk_identifier;
512             # or:
513             $chunk->chunk_identifier($chunk_identifier);
514              
515             (experimental since v0.01)
516              
517             Gets or sets the chunk identifier.
518              
519             B<Note:>
520             The type and range of this value is not yet fully defined and may be changed in later versions of this module.
521              
522             =head2 padding
523              
524             my $padding = $chunk->padding;
525              
526             (experimental since v0.01)
527              
528             Returns the padding status of the chunk.
529              
530             Padding is automatically added by this module as needed.
531              
532             B<Note:>
533             The return type and range is not yet defined.
534             However it is defined that the returned value will be true-ish if any non-zero amount of padding is used and
535             false-ish if no padding is used.
536              
537             B<Note:>
538             This method is hardly useful for most code. It is provided only for debugging.
539              
540             B<Note:>
541             The returned value might or might not reflect the value as read by L</read>.
542             It might be a calculated value.
543              
544             =head2 flag
545              
546             my $bool = $chunk->flag($flag);
547             # or:
548             $chunk->flag($flag => $value); # value is not undef!
549              
550             (since v0.01)
551              
552             Gets or sets the boolean state of a flag.
553              
554             Currently the only supported flag is C<standalone>.
555              
556             This method can be used to set the state of the flag.
557             However L</set_flag> is often the more secure method to do this
558             as it is not tri-state.
559              
560             =head2 set_flag
561              
562             $chunk->set_flag($flag => $value); # any boolean, including undef
563              
564             (since v0.01)
565              
566             Sets the value of a flag.
567             See L</flag> for details on flags.
568              
569             =head2 attach_data
570              
571             $chunk->attach_data($fh [, $offset [, $length]]);
572              
573             (since v0.01)
574              
575             Attaches an open handle as data for a chunk.
576              
577             The passed handle (C<$fh>) will be stored as a reference inside the chunk object.
578              
579             The passed handle must support L<perlfunc/tell>, L<perlfunc/seek>, and L<perlfunc/read>.
580              
581             Optionally a offset and a length can be given to only use a subrange of the data as body for the chunk.
582             This is analogous to L<perlfunc/substr>.
583             If no offset is given the current offset (not the start/offset 0) is used as offset.
584             This is specifically designed to allow stream like reading.
585             If no length is given the length from the offset to the end of the file is used.
586             If C<$fh> refers to a freshly opened file this results in all of the file being used as body by default.
587              
588             B<Note:>
589             The an internal reference to the handle is used to avoid loading the content in memory or into a temporary file.
590             This specifically allowes for chunks with large bodies.
591              
592             B<Note:>
593             While an internal reference is held by the chunk the handle may go out of scope by the caller.
594             However it is not valid to call L<perlfunc/close> on the handle.
595              
596             B<Note:>
597             The handle must be in a 8 bit binary mode before passed to this function.
598             See also L<perlfunc/binmode> regarding binary mode.
599              
600             B<Note:>
601             If the data the handle refers to is altered in the used range (see offset and length) after this method is called
602             the behaviour is undefined.
603              
604             B<Note:>
605             Once the type of a chunk becomes read only this method might also become read only.
606             See L</type> for details.
607              
608             =head2 read_data
609              
610             my $data = $chunk->read_data($length [, $offset]);
611              
612             (experimental since v0.01)
613              
614             Reads data from the body of the chunk.
615              
616             This method reads at most C<$length> bytes at the offset of C<$offset> (defaults to C<0>).
617             The result will be returned.
618              
619             If a read starting beyond the end of the data is requested C<undef> is returned.
620             If less than C<$length> bytes are available a the available bytes are returned.
621              
622             If any invalid values are passed this method will C<die>.
623              
624             B<Note:>
625             This method does not store the current read position.
626             Hence any read that is not to read from the very beginning must provide a C<$offset>.
627              
628             =head1 AUTHOR
629              
630             Philipp Schafft <lion@cpan.org>
631              
632             =head1 COPYRIGHT AND LICENSE
633              
634             This software is Copyright (c) 2025 by Philipp Schafft <lion@cpan.org>.
635              
636             This is free software, licensed under:
637              
638             The Artistic License 2.0 (GPL Compatible)
639              
640             =cut