File Coverage

blib/lib/Cogit/Pack.pm
Criterion Covered Total %
statement 123 128 96.0
branch 30 52 57.6
condition 13 22 59.0
subroutine 15 15 100.0
pod 0 6 0.0
total 181 223 81.1


line stmt bran cond sub pod time code
1             package Cogit::Pack;
2             $Cogit::Pack::VERSION = '0.001001';
3 4     4   1788 use Moo;
  4         6  
  4         23  
4 4     4   7243 use MooX::Types::MooseLike::Base 'InstanceOf';
  4         7  
  4         221  
5 4     4   19 use Path::Class;
  4         5  
  4         170  
6 4     4   17 use Compress::Raw::Zlib;
  4         7  
  4         667  
7 4     4   17 use IO::File;
  4         4  
  4         401  
8 4     4   16 use Carp 'confess';
  4         5  
  4         140  
9 4     4   15 use Check::ISA;
  4         4  
  4         30  
10 4     4   1334 use namespace::clean;
  4         6  
  4         26  
11              
12             has filename => (
13             is => 'ro',
14             isa => InstanceOf['Path::Class::File'],
15             required => 1,
16             coerce => sub { file($_[0]) if !obj($_[0], 'Path::Class::File'); $_[0]; },
17             );
18              
19             has fh => (
20             is => 'rw',
21             isa => InstanceOf['IO::File'],
22             lazy => 1,
23             builder => '_build_fh',
24             );
25              
26             my @TYPES = (qw(none commit tree blob tag), '', 'ofs_delta', 'ref_delta' );
27             my $OBJ_NONE = 0;
28             my $OBJ_COMMIT = 1;
29             my $OBJ_TREE = 2;
30             my $OBJ_BLOB = 3;
31             my $OBJ_TAG = 4;
32             my $OBJ_OFS_DELTA = 6;
33             my $OBJ_REF_DELTA = 7;
34              
35             my $SHA1Size = 20;
36              
37             sub _build_fh {
38 2     2   24 my $self = shift;
39 2   33     21 my $fh = IO::File->new( $self->filename ) || confess($!);
40 2         200 $fh->binmode();
41 2         58 return $fh;
42             }
43              
44             sub all_sha1s {
45 4     4 0 7 my ( $self, $want_sha1 ) = @_;
46 4         51 return Data::Stream::Bulk::Array->new(
47             array => [ $self->index->all_sha1s ] );
48             }
49              
50             sub unpack_object {
51 50     50 0 66 my ( $self, $offset ) = @_;
52 50         47 my $obj_offset = $offset;
53 50         855 my $fh = $self->fh;
54              
55 50 50       362 $fh->seek( $offset, 0 ) || die "Error seeking in pack: $!";
56 50 50       315 $fh->read( my $c, 1 ) || die "Error reading from pack: $!";
57 50   50     424 $c = unpack( 'C', $c ) || die $!;
58              
59 50         50 my $size = ( $c & 0xf );
60 50         54 my $type_number = ( $c >> 4 ) & 7;
61 50   33     106 my $type = $TYPES[$type_number] || confess "invalid type $type_number";
62              
63 50         34 my $shift = 4;
64 50         46 $offset++;
65              
66 50         99 while ( ( $c & 0x80 ) != 0 ) {
67 42 50       83 $fh->read( $c, 1 ) || die $!;
68 42   50     227 $c = unpack( 'C', $c ) || die $!;
69 42         65 $size |= ( ( $c & 0x7f ) << $shift );
70 42         34 $shift += 7;
71 42         89 $offset += 1;
72             }
73              
74 50 100 100     369 if ( $type eq 'ofs_delta' || $type eq 'ref_delta' ) {
    50 100        
      66        
      33        
75 4         35 ( $type, $size, my $content )
76             = $self->unpack_deltified( $type, $offset, $obj_offset, $size );
77 4         13 return ( $type, $size, $content );
78              
79             } elsif ( $type eq 'commit'
80             || $type eq 'tree'
81             || $type eq 'blob'
82             || $type eq 'tag' )
83             {
84 46         91 my $content = $self->read_compressed( $offset, $size );
85 46         159 return ( $type, $size, $content );
86             } else {
87 0         0 confess "invalid type $type";
88             }
89             }
90              
91             sub read_compressed {
92 50     50 0 51 my ( $self, $offset, $size ) = @_;
93 50         860 my $fh = $self->fh;
94              
95 50 50       228 $fh->seek( $offset, 0 ) || die $!;
96 50         477 my ( $deflate, $status ) = Compress::Raw::Zlib::Inflate->new(
97             -AppendOutput => 1,
98             -ConsumeInput => 0
99             );
100              
101 50         10650 my $out = "";
102 50         113 while ( length($out) < $size ) {
103 50 50       121 $fh->read( my $block, 4096 ) || die $!;
104 50         918 my $status = $deflate->inflate( $block, $out );
105             }
106 50 50       81 confess length($out)." is not $size" unless length($out) == $size;
107              
108 50 50       174 $fh->seek( $offset + $deflate->total_in, 0 ) || die $!;
109 50         418 return $out;
110             }
111              
112             sub unpack_deltified {
113 4     4 0 11 my ( $self, $type, $offset, $obj_offset, $size ) = @_;
114 4         91 my $fh = $self->fh;
115              
116 4         21 my $base;
117              
118 4 50       15 $fh->seek( $offset, 0 ) || die $!;
119 4 50       44 $fh->read( my $data, $SHA1Size ) || die $!;
120 4         45 my $sha1 = unpack( 'H*', $data );
121              
122 4 100       14 if ( $type eq 'ofs_delta' ) {
123 2         11 my $i = 0;
124 2         8 my $c = unpack( 'C', substr( $data, $i, 1 ) );
125 2         4 my $base_offset = $c & 0x7f;
126              
127 2         14 while ( ( $c & 0x80 ) != 0 ) {
128 2         10 $c = unpack( 'C', substr( $data, ++$i, 1 ) );
129 2         5 $base_offset++;
130 2         8 $base_offset <<= 7;
131 2         8 $base_offset |= $c & 0x7f;
132             }
133 2         5 $base_offset = $obj_offset - $base_offset;
134 2         4 $offset += $i + 1;
135              
136 2         12 ( $type, undef, $base ) = $self->unpack_object($base_offset);
137             } else {
138 2         13 ( $type, undef, $base ) = $self->get_object($sha1);
139 2         3 $offset += $SHA1Size;
140              
141             }
142              
143 4         10 my $delta = $self->read_compressed( $offset, $size );
144 4         32 my $new = $self->patch_delta( $base, $delta );
145              
146 4         10 return ( $type, length($new), $new );
147             }
148              
149             sub patch_delta {
150 4     4 0 9 my ( $self, $base, $delta ) = @_;
151              
152 4         19 my ( $src_size, $pos ) = $self->patch_delta_header_size( $delta, 0 );
153 4 50       9 if ( $src_size != length($base) ) {
154 0         0 confess "invalid delta data";
155             }
156              
157 4         8 ( my $dest_size, $pos ) = $self->patch_delta_header_size( $delta, $pos );
158 4         11 my $dest = "";
159              
160 4         10 while ( $pos < length($delta) ) {
161 12         15 my $c = substr( $delta, $pos, 1 );
162 12         11 $c = unpack( 'C', $c );
163 12         10 $pos++;
164 12 100       27 if ( ( $c & 0x80 ) != 0 ) {
    50          
165              
166 4         3 my $cp_off = 0;
167 4         4 my $cp_size = 0;
168 4 50       17 $cp_off = unpack( 'C', substr( $delta, $pos++, 1 ) )
169             if ( $c & 0x01 ) != 0;
170 4 50       12 $cp_off |= unpack( 'C', substr( $delta, $pos++, 1 ) ) << 8
171             if ( $c & 0x02 ) != 0;
172 4 50       10 $cp_off |= unpack( 'C', substr( $delta, $pos++, 1 ) ) << 16
173             if ( $c & 0x04 ) != 0;
174 4 50       9 $cp_off |= unpack( 'C', substr( $delta, $pos++, 1 ) ) << 24
175             if ( $c & 0x08 ) != 0;
176 4 50       13 $cp_size = unpack( 'C', substr( $delta, $pos++, 1 ) )
177             if ( $c & 0x10 ) != 0;
178 4 50       9 $cp_size |= unpack( 'C', substr( $delta, $pos++, 1 ) ) << 8
179             if ( $c & 0x20 ) != 0;
180 4 50       10 $cp_size |= unpack( 'C', substr( $delta, $pos++, 1 ) ) << 16
181             if ( $c & 0x40 ) != 0;
182 4 50       9 $cp_size = 0x10000 if $cp_size == 0;
183              
184 4         11 $dest .= substr( $base, $cp_off, $cp_size );
185             } elsif ( $c != 0 ) {
186 8         16 $dest .= substr( $delta, $pos, $c );
187 8         13 $pos += $c;
188             } else {
189 0         0 confess 'invalid delta data';
190             }
191             }
192              
193 4 50       12 if ( length($dest) != $dest_size ) {
194 0         0 confess 'invalid delta data';
195             }
196 4         7 return $dest;
197             }
198              
199             sub patch_delta_header_size {
200 8     8 0 8 my ( $self, $delta, $pos ) = @_;
201              
202 8         12 my $size = 0;
203 8         8 my $shift = 0;
204 8         9 while (1) {
205              
206 16         16 my $c = substr( $delta, $pos, 1 );
207 16 50       21 unless ( defined $c ) {
208 0         0 confess 'invalid delta header';
209             }
210 16         20 $c = unpack( 'C', $c );
211              
212 16         14 $pos++;
213 16         15 $size |= ( $c & 0x7f ) << $shift;
214 16         9 $shift += 7;
215 16 100       35 last if ( $c & 0x80 ) == 0;
216             }
217 8         13 return ( $size, $pos );
218             }
219              
220             1;
221              
222             __END__
223              
224             =pod
225              
226             =encoding UTF-8
227              
228             =head1 NAME
229              
230             Cogit::Pack
231              
232             =head1 VERSION
233              
234             version 0.001001
235              
236             =head1 AUTHOR
237              
238             Arthur Axel "fREW" Schmidt <cogit@afoolishmanifesto.com>
239              
240             =head1 COPYRIGHT AND LICENSE
241              
242             This software is copyright (c) 2017 by Arthur Axel "fREW" Schmidt.
243              
244             This is free software; you can redistribute it and/or modify it under
245             the same terms as the Perl 5 programming language system itself.
246              
247             =cut