File Coverage

blib/lib/DataStore/CAS/FS/DirCodec/Unix.pm
Criterion Covered Total %
statement 93 114 81.5
branch 35 52 67.3
condition 12 21 57.1
subroutine 18 34 52.9
pod 2 2 100.0
total 160 223 71.7


line stmt bran cond sub pod time code
1             package DataStore::CAS::FS::DirCodec::Unix;
2 5     5   475 use 5.008;
  5         11  
3 5     5   17 use strict;
  5         4  
  5         69  
4 5     5   15 use warnings;
  5         4  
  5         88  
5 5     5   13 use Try::Tiny;
  5         4  
  5         209  
6 5     5   16 use Carp;
  5         3  
  5         212  
7 5     5   35 use JSON 2.53 ();
  5         52  
  5         90  
8 5     5   16 use Scalar::Util 'looks_like_number';
  5         4  
  5         306  
9             require DataStore::CAS::FS::Dir;
10             require DataStore::CAS::FS::DirEnt;
11             require DataStore::CAS::FS::InvalidUTF8;
12             *decode_utf8= *DataStore::CAS::FS::InvalidUTF8::decode_utf8;
13              
14 5     5   335 use parent 'DataStore::CAS::FS::DirCodec';
  5         194  
  5         18  
15              
16             our $VERSION= '0.011000';
17              
18             __PACKAGE__->register_format(unix => __PACKAGE__);
19              
20             # ABSTRACT: Efficiently encode only the attributes of a UNIX stat()
21              
22              
23             our $_json_coder;
24             sub _build_json_coder {
25 1     1   26 DataStore::CAS::FS::InvalidUTF8->add_json_filter(
26             JSON->new->utf8->canonical->convert_blessed, 1
27             );
28             }
29              
30             our %_TypeToCode= (
31             file => ord('f'), dir => ord('d'), symlink => ord('l'),
32             chardev => ord('c'), blockdev => ord('b'),
33             pipe => ord('p'), socket => ord('s'), whiteout => ord('w'),
34             );
35             our %_CodeToType= map { $_TypeToCode{$_} => $_ } keys %_TypeToCode;
36             our @_FieldOrder= qw(
37             type name ref size modify_ts unix_uid unix_gid unix_mode metadata_ts
38             access_ts unix_nlink unix_dev unix_inode unix_blocksize unix_blockcount
39             );
40              
41             sub encode {
42 9     9 1 2697 my ($class, $entry_list, $metadata)= @_;
43 9 50       24 $metadata= defined($metadata)? { %$metadata } : {};
44             defined $metadata->{_}
45 9 50       20 and croak '$metadata{_} is reserved for the directory encoder';
46 9         6 my (%umap, %gmap);
47             my @entries= map {
48 9 50       15 my $e= ref $_ eq 'HASH'? $_ : $_->as_hash;
  16         28  
49             defined $e->{type}
50 16 100       173 or croak "'type' attribute is required";
51             my $code= $_TypeToCode{$e->{type}}
52 15 100       119 or croak "Unknown directory entry type: ".$e->{type};
53              
54 14         16 my $name= $e->{name};
55 14 100       107 defined $name
56             or croak "'name' attribute is required";
57 13 100       13 _make_utf8($name)
58             or croak "'name' must be a unicode scalar or an InvalidUTF8 instance";
59              
60 12         12 my $ref= $e->{ref};
61 12 100       18 $ref= '' unless defined $ref;
62 12 100       11 _make_utf8($ref)
63             or croak "'ref' must be a unicode scalar or an InvalidUTF8 instance";
64              
65             $umap{$e->{unix_uid}}= $e->{unix_user}
66 11 50 66     33 if defined $e->{unix_uid} && defined $e->{unix_user};
67             $gmap{$e->{unix_gid}}= $e->{unix_group}
68 11 50 66     24 if defined $e->{unix_gid} && defined $e->{unix_group};
69              
70             my $int_attr_str= join(":",
71 132 50       171 map { !defined $_? '' : looks_like_number($_)? $_ : croak "Invalid unix attribute number: $_" }
    100          
72 11         16 @{$e}{@_FieldOrder[3..$#_FieldOrder]}
  11         42  
73             );
74             # As an optimization, all undef trailing fields can be chopped off.
75 11         33 $int_attr_str =~ s/:+$//;
76            
77 11 50       20 croak "'name' too long: '$name'" if length($name) > 255;
78 11 50       16 croak "'ref' too long: '$ref'" if length($ref) > 255;
79 11 50       15 croak "Unix fields too long: '$int_attr_str'" if length($int_attr_str) > 255;
80 11         38 pack('CCCC', length($name), length($ref), length($int_attr_str), $code).$name."\0".$ref."\0".$int_attr_str;
81             } @$entry_list;
82              
83             # Save the mapping of UID to User and GID to Group
84 4         10 $metadata->{_}{umap}= \%umap;
85 4         6 $metadata->{_}{gmap}= \%gmap;
86            
87 4   66     29 my $meta_json= ($_json_coder ||= _build_json_coder())->encode($metadata);
88             my $ret= "CAS_Dir 04 unix\n"
89             .pack('N', length($meta_json)).$meta_json
90 4         21 .join('', sort { substr($a,4) cmp substr($b,4) } @entries);
  16         20  
91 4 50       10 croak "Accidental unicode concatenation"
92             if utf8::is_utf8($ret);
93 4         14 $ret;
94             }
95              
96             # Convert string in-place to utf-8 bytes, or return false.
97             # A less speed-obfuscated version might read:
98             # my $str= shift;
99             # if (ref $str) {
100             # return 0 unless ref($str)->can('TO_UTF8');
101             # $_[0]= $str->TO_UTF8;
102             # return 1;
103             # } elsif (utf8::is_utf8($str)) {
104             # utf8::encode($_[0]);
105             # return 1;
106             # } else {
107             # return !($_[0] =~ /[\x7F-\xFF]/);
108             # }
109             sub _make_utf8 {
110 25 100 33 25   318 ref $_[0]?
      100        
111             (ref($_[0])->can('TO_UTF8') && (($_[0]= $_[0]->TO_UTF8) || 1))
112             : &utf8::is_utf8 && (&utf8::encode || 1) || !($_[0] =~ /[\x80-\xFF]/);
113             }
114              
115              
116             sub decode {
117 4     4 1 309 my ($class, $params)= @_;
118             $params->{format}= $class->_read_format($params)
119 4 100       15 unless defined $params->{format};
120 4         4 my $handle= $params->{handle};
121 4 50       6 if (!$handle) {
122 0 0       0 if (defined $params->{data}) {
123             open($handle, '<', \$params->{data})
124 0 0       0 or croak "can't open handle to scalar";
125             } else {
126 0         0 $handle= $params->{file}->open;
127             }
128             }
129              
130 4         13 my $header_len= $class->_calc_header_length($params->{format});
131 4 50       11 seek($handle, $header_len, 0) or croak "seek: $!";
132              
133 4         3 my (@entries, $buf, $pos);
134              
135             # first, pull out the metadata, which includes the UID map and GID map.
136 4         9 $class->_readall($handle, $buf, 4);
137 4         16 my ($dirmeta_len)= unpack('N', $buf);
138 4         9 $class->_readall($handle, my $json, $dirmeta_len);
139 4   33     36 my $meta= ($_json_coder ||= _build_json_coder())->decode($json);
140              
141             # Quick sanity checks
142             ref $meta->{_}{umap} and ref $meta->{_}{gmap}
143 4 50 33     19 or croak "Incorrect directory metadata";
144 4         4 my $dirmeta= delete $meta->{_};
145              
146 4         10 while (!eof $handle) {
147 11         17 $class->_readall($handle, $buf, 4);
148 11         35 my ($name_len, $ref_len, $meta_len, $code)= unpack('CCCC', $buf);
149 11         35 $class->_readall($handle, $buf, $name_len+$ref_len+$meta_len+2);
150             my @fields= (
151             $dirmeta,
152             $code,
153             DataStore::CAS::FS::InvalidUTF8->decode_utf8(substr($buf, 0, $name_len)),
154             $ref_len? DataStore::CAS::FS::InvalidUTF8->decode_utf8(substr($buf, $name_len+1, $ref_len)) : undef,
155 11 100       36 map { length($_)? $_ : undef } split(":", substr($buf, $name_len+$ref_len+2, $meta_len)),
  27 100       69  
156             );
157 11         50 push @entries, bless(\@fields, __PACKAGE__.'::Entry');
158             }
159 4         7 close $handle;
160             return DataStore::CAS::FS::Dir->new(
161             file => $params->{file},
162             format => $params->{format},
163 4         45 metadata => $meta,
164             entries => \@entries,
165             );
166             }
167              
168             package DataStore::CAS::FS::DirCodec::Unix::Entry;
169 5     5   4647 use strict;
  5         7  
  5         83  
170 5     5   13 use warnings;
  5         5  
  5         110  
171 5     5   13 use parent 'DataStore::CAS::FS::DirEnt';
  5         5  
  5         18  
172              
173 0     0   0 sub _dirmeta { $_[0][0] }
174 11     11   35 sub type { $_CodeToType{$_[0][1]} }
175 1     1   4 sub name { $_[0][2] }
176 0     0   0 sub ref { $_[0][3] }
177 0     0   0 sub size { $_[0][4] }
178 0     0   0 sub modify_ts { $_[0][5] }
179 0     0   0 sub unix_uid { $_[0][6] }
180 0     0   0 sub unix_gid { $_[0][7] }
181 0     0   0 sub unix_mode { $_[0][8] }
182 0     0   0 sub metadata_ts { $_[0][9] }
183 0     0   0 sub access_ts { $_[0][10] }
184 0     0   0 sub unix_nlink { $_[0][11] }
185 0     0   0 sub unix_dev { $_[0][12] }
186 0     0   0 sub unix_inode { $_[0][13] }
187 0     0   0 sub unix_blocksize { $_[0][14] }
188 0     0   0 sub unix_blockcount { $_[0][15] }
189              
190             *unix_mtime= *modify_ts;
191             *unix_atime= *access_ts;
192             *unix_ctime= *metadata_ts;
193 0     0   0 sub unix_user { my $self= shift; $self->_dirmeta->{umap}{ $self->unix_uid } }
  0         0  
194 0     0   0 sub unix_group { my $self= shift; $self->_dirmeta->{gmap}{ $self->unix_gid } }
  0         0  
195              
196             sub as_hash {
197 11     11   13 my $self= shift;
198             return {
199             type => $self->type,
200 11         15 map { $_FieldOrder[$_-1] => $self->[$_] } grep { defined $self->[$_] } 2 .. $#$self
  35         94  
  49         57  
201             };
202             }
203              
204             1;
205              
206             __END__