File Coverage

blib/lib/DataStore/CAS/FS/DirCodec/Unix.pm
Criterion Covered Total %
statement 94 115 81.7
branch 35 52 67.3
condition 12 21 57.1
subroutine 18 34 52.9
pod 2 2 100.0
total 161 224 71.8


line stmt bran cond sub pod time code
1             package DataStore::CAS::FS::DirCodec::Unix;
2 5     5   1029 use 5.008;
  5         15  
  5         185  
3 5     5   27 use strict;
  5         9  
  5         133  
4 5     5   26 use warnings;
  5         10  
  5         119  
5 5     5   27 use Try::Tiny;
  5         8  
  5         335  
6 5     5   25 use Carp;
  5         7  
  5         376  
7 5     5   24 use JSON 2.53 ();
  5         79  
  5         116  
8 5     5   25 use Scalar::Util 'looks_like_number';
  5         8  
  5         418  
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   842 use parent 'DataStore::CAS::FS::DirCodec';
  5         327  
  5         33  
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   77 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 7129 my ($class, $entry_list, $metadata)= @_;
43 9 50       43 $metadata= defined($metadata)? { %$metadata } : {};
44 9 50       35 defined $metadata->{_}
45             and croak '$metadata{_} is reserved for the directory encoder';
46 9         16 my (%umap, %gmap);
47 16 50       52 my @entries= map {
48 9         25 my $e= ref $_ eq 'HASH'? $_ : $_->as_hash;
49 16 100       299 defined $e->{type}
50             or croak "'type' attribute is required";
51 15 100       341 my $code= $_TypeToCode{$e->{type}}
52             or croak "Unknown directory entry type: ".$e->{type};
53              
54 14         23 my $name= $e->{name};
55 14 100       408 defined $name
56             or croak "'name' attribute is required";
57 13 100       32 _make_utf8($name)
58             or croak "'name' must be a unicode scalar or an InvalidUTF8 instance";
59              
60 12         23 my $ref= $e->{ref};
61 12 100       25 $ref= '' unless defined $ref;
62 12 100       18 _make_utf8($ref)
63             or croak "'ref' must be a unicode scalar or an InvalidUTF8 instance";
64              
65 11 50 66     65 $umap{$e->{unix_uid}}= $e->{unix_user}
66             if defined $e->{unix_uid} && defined $e->{unix_user};
67 11 50 66     36 $gmap{$e->{unix_gid}}= $e->{unix_group}
68             if defined $e->{unix_gid} && defined $e->{unix_group};
69              
70 132 50       446 my $int_attr_str= join(":",
    100          
71 11         104 map { !defined $_? '' : looks_like_number($_)? $_ : croak "Invalid unix attribute number: $_" }
72 11         34 @{$e}{@_FieldOrder[3..$#_FieldOrder]}
73             );
74             # As an optimization, all undef trailing fields can be chopped off.
75 11         58 $int_attr_str =~ s/:+$//;
76            
77 11 50       31 croak "'name' too long: '$name'" if length($name) > 255;
78 11 50       25 croak "'ref' too long: '$ref'" if length($ref) > 255;
79 11 50       25 croak "Unix fields too long: '$int_attr_str'" if length($int_attr_str) > 255;
80 11         68 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         98 $metadata->{_}{umap}= \%umap;
85 4         12 $metadata->{_}{gmap}= \%gmap;
86            
87 4   66     72 my $meta_json= ($_json_coder ||= _build_json_coder())->encode($metadata);
88 16         34 my $ret= "CAS_Dir 04 unix\n"
89             .pack('N', length($meta_json)).$meta_json
90 4         42 .join('', sort { substr($a,4) cmp substr($b,4) } @entries);
91 4 50       17 croak "Accidental unicode concatenation"
92             if utf8::is_utf8($ret);
93 4         28 $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   1066 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 546 my ($class, $params)= @_;
118 4 100       30 $params->{format}= $class->_read_format($params)
119             unless defined $params->{format};
120 4         11 my $handle= $params->{handle};
121 4 50       12 if (!$handle) {
122 0 0       0 if (defined $params->{data}) {
123 0 0       0 open($handle, '<', \$params->{data})
124             or croak "can't open handle to scalar";
125             } else {
126 0         0 $handle= $params->{file}->open;
127             }
128             }
129              
130 4         25 my $header_len= $class->_calc_header_length($params->{format});
131 4 50       31 seek($handle, $header_len, 0) or croak "seek: $!";
132              
133 4         8 my (@entries, $buf, $pos);
134              
135             # first, pull out the metadata, which includes the UID map and GID map.
136 4         22 $class->_readall($handle, $buf, 4);
137 4         38 my ($dirmeta_len)= unpack('N', $buf);
138 4         21 $class->_readall($handle, my $json, $dirmeta_len);
139 4   33     72 my $meta= ($_json_coder ||= _build_json_coder())->decode($json);
140              
141             # Quick sanity checks
142 4 50 33     39 ref $meta->{_}{umap} and ref $meta->{_}{gmap}
143             or croak "Incorrect directory metadata";
144 4         14 my $dirmeta= delete $meta->{_};
145              
146 4         19 while (!eof $handle) {
147 11         112 $class->_readall($handle, $buf, 4);
148 11         140 my ($name_len, $ref_len, $meta_len, $code)= unpack('CCCC', $buf);
149 11         73 $class->_readall($handle, $buf, $name_len+$ref_len+$meta_len+2);
150 27 100       110 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       71 map { length($_)? $_ : undef } split(":", substr($buf, $name_len+$ref_len+2, $meta_len)),
156             );
157 11         108 push @entries, bless(\@fields, __PACKAGE__.'::Entry');
158             }
159 4         13 close $handle;
160 4         54 return DataStore::CAS::FS::Dir->new(
161             file => $params->{file},
162             format => $params->{format},
163             metadata => $meta,
164             entries => \@entries,
165             );
166             }
167              
168             package DataStore::CAS::FS::DirCodec::Unix::Entry;
169 5     5   7657 use strict;
  5         12  
  5         162  
170 5     5   49 use warnings;
  5         10  
  5         174  
171 5     5   25 use parent 'DataStore::CAS::FS::DirEnt';
  5         9  
  5         26  
172              
173 0     0   0 sub _dirmeta { $_[0][0] }
174 11     11   73 sub type { $_CodeToType{$_[0][1]} }
175 1     1   6 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   95 my $self= shift;
198             return {
199 35         181 type => $self->type,
200 11         33 map { $_FieldOrder[$_-1] => $self->[$_] } grep { defined $self->[$_] } 2 .. $#$self
  49         226  
201             };
202             }
203              
204             1;
205              
206             __END__