File Coverage

blib/lib/Sys/Export.pm
Criterion Covered Total %
statement 138 170 81.1
branch 62 112 55.3
condition 19 43 44.1
subroutine 32 40 80.0
pod 26 28 92.8
total 277 393 70.4


line stmt bran cond sub pod time code
1             package Sys::Export;
2              
3             our $VERSION = '0.006'; # VERSION
4             # ABSTRACT: Export a subset of an OS file tree, for chroot/initrd
5              
6 19     19   1132401 use v5.26;
  19         54  
7 19     19   80 use warnings;
  19         21  
  19         866  
8 19     19   125 use experimental qw( signatures );
  19         24  
  19         107  
9 19     19   2037 use Carp;
  19         87  
  19         1088  
10 19     19   99 use Scalar::Util qw( blessed looks_like_number );
  19         22  
  19         838  
11 19     19   7206 use Sys::Export::LogAny '$log';
  19         48  
  19         142  
12 19     19   3255 use Exporter ();
  19         41  
  19         331  
13 19     19   59 use Fcntl ':mode';
  19         28  
  19         9305  
14             BEGIN {
15             # Fcntl happily exports macros that don't exist, then fails at runtime.
16             # Replace non-existent test macros with 'false', and nonexistent modes with 0.
17             # But, on MSWin32, the constant for S_IFLNK is 0x4000 and just isn't defined in Fcntl
18             # because the headers don't define it.
19 19     19   98 for (qw( S_ISREG S_ISDIR S_ISLNK S_ISBLK S_ISCHR S_ISFIFO S_ISSOCK S_ISWHT
20             S_IFREG S_IFDIR S_IFLNK S_IFBLK S_IFCHR S_IFIFO S_IFSOCK S_IFWHT S_IFMT )
21             ) {
22 323 100       286 next if eval { __PACKAGE__->can($_)->($_ =~ /_IS/? (0) : ()); 1 };
  323 100       2550  
  285         59705  
23 38         107 delete ${Sys::Export::}{$_};
24 38 50 33     254 if ($^O eq 'MSWin32' && $_ eq 'S_IFLNK') {
    50 33        
25 0 0       0 eval 'sub Sys::Export::S_IFLNK { 0x6000 } 1' or die "S_IFLNK $@";
26             } elsif ($^O eq 'MSWin32' && $_ eq 'S_ISLNK') {
27 0 0       0 eval 'sub Sys::Export::S_ISLNK { S_IFMT($_[0]) == 0x6000 } 1' or die "S_ISLNK $@";
28             } else {
29 38 50   5 0 3725 eval "sub Sys::Export::$_ { 0 } 1" or die "$@";
  5     0 0 46  
  0         0  
30             }
31             }
32             }
33             our @EXPORT_OK= qw(
34             isa_exporter isa_export_dst isa_userdb isa_user isa_group exporter isa_hash isa_array isa_int
35             isa_handle isa_pow2 isa_data_ref round_up_to_pow2 round_up_to_multiple map_or_load_file filedata
36             add skip find which finish rewrite_path rewrite_user rewrite_group expand_stat_shorthand
37             write_file_extent _pack _unpack
38             S_ISREG S_ISDIR S_ISLNK S_ISBLK S_ISCHR S_ISFIFO S_ISSOCK S_ISWHT
39             S_IFREG S_IFDIR S_IFLNK S_IFBLK S_IFCHR S_IFIFO S_IFSOCK S_IFWHT S_IFMT
40             );
41             our %EXPORT_TAGS= (
42             basic_methods => [qw( exporter add skip find which finish rewrite_path rewrite_user rewrite_group filedata )],
43             isa => [qw( isa_exporter isa_export_dst isa_userdb isa_user isa_group isa_hash isa_array isa_handle isa_int isa_pow2 isa_data_ref )],
44             stat_modes => [qw( S_IFREG S_IFDIR S_IFLNK S_IFBLK S_IFCHR S_IFIFO S_IFSOCK S_IFWHT S_IFMT )],
45             stat_tests => [qw( S_ISREG S_ISDIR S_ISLNK S_ISBLK S_ISCHR S_ISFIFO S_ISSOCK S_ISWHT )],
46             );
47             my ($is_module_name, $require_module);
48              
49             # optional dependency on Module::Runtime. This way if there's any bug in my cheap
50             # substitute, the fix is to just install the official module.
51             if (eval { require Module::Runtime; }) {
52             $is_module_name= \&Module::Runtime::is_module_name;
53             $require_module= \&Module::Runtime::require_module;
54             } else {
55             $is_module_name= sub { $_[0] =~ /^[A-Z_a-z][0-9A-Z_a-z]*(?:::[0-9A-Z_a-z]+)*\z/ };
56             $require_module= sub { require( ($_[0] =~ s{::}{/}gr).'.pm' ) };
57             }
58              
59              
60             sub import {
61 47     47   260 my $class= $_[0];
62 47         85 my $caller= caller;
63 47         82 my %ctor_opts;
64 47         174 for (my $i= 1; $i < $#_; ++$i) {
65 125 50       411 if (ref $_[$i] eq 'HASH') {
    100          
66 0         0 %ctor_opts= ( %ctor_opts, %{ splice(@_, $i--, 1) } );
  0         0  
67             }
68             elsif ($_[$i] =~ /^-(type|src|dst|tmp|src_userdb|dst_userdb|rewrite_path|rewrite_user|rewrite_group)\z/) {
69 3         10 $ctor_opts{$1}= (splice @_, $i--, 2)[1];
70             }
71             }
72 47 100       107 if (keys %ctor_opts) {
73 1         2 init_global_exporter(%ctor_opts);
74             # caller requested the global exporter instance, so also include the standard methods
75             # unless it looks like they were more selective about what to import.
76 1 50       9 push @_, 'exporter', ':basic_methods'
77             unless grep /^(add|:.*methods)\z/, @_;
78             }
79 47         191324 goto \&Exporter::import;
80             }
81              
82             our $exporter;
83 0     0 1 0 sub exporter { $exporter }
84              
85             our %osname_to_class= (
86             linux => 'Linux',
87             );
88              
89 1     1 1 2 sub init_global_exporter(%config) {
  1         2  
  1         2  
90 1   33     3 my $type= delete $config{type} // $^O;
91             # remap known OS names
92 1   33     4 my $class= $osname_to_class{$type} // $type;
93             # prefix bare names with namespace
94 1 50       4 $class= "Sys::Export::$class" unless $class =~ /::/;
95 1 50       2 $is_module_name->($class) or croak "Invalid module name '$class'";
96             # if it fails, die with 'croak'
97 1 50       41 eval { $require_module->($class) } or croak "$@";
  1         2  
98             # now construct one
99 1         7 $exporter= $class->new(%config);
100             }
101              
102              
103 3     3 1 15 sub add { $exporter->add(@_) }
104 1     1 1 3 sub skip { $exporter->skip(@_) }
105 1     1 1 8 sub find { $exporter->src_find(@_) }
106 0     0 1 0 sub which :prototype($) { $exporter->src_which(@_) }
107 1     1 1 3 sub finish { $exporter->finish(@_) }
108 0     0 1 0 sub rewrite_path { $exporter->rewrite_path(@_) }
109 0     0 1 0 sub rewrite_user { $exporter->rewrite_user(@_) }
110 0     0 1 0 sub rewrite_group { $exporter->rewrite_group(@_) }
111              
112              
113 10646     10646 1 22102 sub isa_hash :prototype($) { ref $_[0] eq 'HASH' }
114 21107     21107 1 46632 sub isa_array :prototype($) { ref $_[0] eq 'ARRAY' }
115 35 50 33 35 1 405 sub isa_handle :prototype($) { ref $_[0] eq 'GLOB' || (ref $_[0] && (ref $_[0])->isa('IO::Handle')) }
116 31861 100   31861 1 96069 sub isa_int :prototype($) { looks_like_number($_[0]) && int($_[0]) == $_[0] }
117 0 0   0 1 0 sub isa_exporter :prototype($) { blessed($_[0]) && $_[0]->isa('Sys::Export::Exporter') }
118 14 100 66 14 1 213 sub isa_export_dst :prototype($) { blessed($_[0]) && $_[0]->can('add') && $_[0]->can('finish') }
119 11 100 66 11 1 59 sub isa_userdb :prototype($) { blessed($_[0]) && $_[0]->can('user') && $_[0]->can('group') }
120 18 100   18 1 72 sub isa_user :prototype($) { blessed($_[0]) && $_[0]->isa('Sys::Export::Unix::UserDB::User') }
121 18 100   18 1 68 sub isa_group :prototype($) { blessed($_[0]) && $_[0]->isa('Sys::Export::Unix::UserDB::Group') }
122 1330     1330 1 3530 sub isa_pow2 :prototype($) { !($_[0] & ($_[0]-1)) }
123 24 50 0 24 1 67 sub isa_data_ref :prototype($) { ref $_[0] eq 'SCALAR' || blessed($_[0]) && $_[0]->can('as_scalarref') }
124              
125              
126 4     4   4 sub _parse_major_minor_data($attrs, $data) {
  4         5  
  4         4  
  4         3  
127 4 100       7 @{$attrs}{'rdev_major','rdev_minor'}= isa_array $data? @$data : split(/[,:]/, $data);
  4         10  
128             }
129             our %_mode_alias= (
130             file => [ S_IFREG, sub { 0666 & ~umask } ],
131             dir => [ S_IFDIR, sub { 0777 & ~umask } ],
132             sym => [ S_IFLNK, sub { 0777 } ],
133             blk => [ S_IFBLK, sub { 0666 & ~umask }, \&_parse_major_minor_data, ],
134             chr => [ S_IFCHR, sub { 0666 & ~umask }, \&_parse_major_minor_data, ],
135             fifo => [ S_IFIFO, sub { 0666 & ~umask } ],
136             sock => [ S_IFSOCK, sub { 0666 & ~umask } ],
137             );
138             our @_mode_by_int;
139             $_mode_by_int[$_->[0]]= $_ for values %_mode_alias;
140             $_mode_by_int[0]= undef; # don't map 0 to any mode
141              
142             sub expand_stat_shorthand {
143 10543 100 66 10543 1 104644 @_= @{$_[0]} if @_ == 1 && isa_array $_[0];
  10534         18950  
144 10543 100 100     22413 my %attrs= @_ > 2 && isa_hash $_[-1]? %{ pop @_ } : ();
  46         115  
145 10543         14258 my ($mode, $name, $data)= @_;
146 10543         10302 my $mode_desc;
147 10543 50       12460 if (isa_int $mode) {
148 0 0       0 $mode_desc= $_mode_by_int[$mode & S_IFMT]
149             or carp sprintf("Numeric mode %x doesn't match any known node types", $mode);
150             }
151             else {
152 10543 50       29010 $mode =~ /^([a-z]+)([0-7]+)?\z/
153             or croak "Invalid mode '$mode': expected number, or prefix file/dir/sym/blk/chr/fifo/sock followed by octal permissions";
154 10543 50       22454 $mode_desc= $_mode_alias{$1}
155             or croak "Unknown mode alias '$1'";
156 10543 100       20512 $mode= $mode_desc->[0] | (defined $2? oct($2) : $mode_desc->[1]->());
157             }
158 10543         16489 $attrs{mode}= $mode;
159 10543 50       18066 length $name or croak "Name must be nonzero length";
160 10543         12714 $attrs{name}= $name;
161 10543 100       13359 if (defined $data) {
162 10533 100 66     21886 if ($mode_desc && $mode_desc->[2]) {
163 4         8 $mode_desc->[2]->(\%attrs, $data);
164             } else {
165 10529         12297 $attrs{data}= $data;
166             }
167             }
168 10543         39169 return %attrs;
169             }
170              
171              
172 0     0 1 0 sub round_up_to_pow2($n) {
  0         0  
  0         0  
173 0 0       0 croak "Not defined for negative numbers" unless $n > 0;
174 0 0       0 return 1 if $n <= 1;
175 0         0 --$n;
176 0         0 $n |= $n >> 1;
177 0         0 $n |= $n >> 2;
178 0         0 $n |= $n >> 4;
179 0         0 $n |= $n >> 8;
180 0         0 $n |= $n >> 16;
181 0         0 $n |= $n >> 32;
182 0         0 return $n+1;
183             }
184              
185 62     62 1 79 sub round_up_to_multiple($n, $pow2) {
  62         85  
  62         59  
  62         55  
186 62 50       87 croak "Not defined for negative numbers" unless $n > 0;
187 62         68 my $mask= $pow2-1;
188 62         138 return ($n + $mask) & ~$mask;
189             }
190              
191              
192             if (eval { require File::Map; }) {
193             eval q{
194             sub map_or_load_file($filename, $offset=0, $length=undef) {
195             my $buf;
196             defined $length? File::Map::map_file($buf, $filename, "<", $offset, $length)
197             : File::Map::map_file($buf, $filename, "<", $offset, $length);
198             return \$buf;
199             }
200             1;
201             } or die "$@";
202             } else {
203             *map_or_load_file= *_load_file;
204             }
205              
206 26     26   27 sub _load_file($filename, $offset= 0, $length= undef) {
  26         30  
  26         31  
  26         23  
  26         32  
207 26 50       757 open my $fh, "<:raw", $filename
208             or die "open($filename): $!";
209 26         187 my $size= -s $fh;
210 26 50       46 croak "Offset beyond end of file ($filename, $offset > $size)" if $offset > $size;
211 26   33     42 $length //= $size - $offset;
212 26         35 my $buf= '';
213 26 50       39 if ($length) {
214 26 50       59 if ($offset > 0) {
215 0 0       0 sysseek($fh, $offset, 0) == $offset
216             or croak "sysseek($filename, $offset): $!";
217             }
218 26 50       426 sysread($fh, $buf, $length) == $size
219             or die "sysread($filename, $size): $!";
220             }
221 26         408 \$buf;
222             }
223              
224              
225             sub filedata {
226 23     23 1 80590 state $loaded= require Sys::Export::LazyFileData;
227 23         113 Sys::Export::LazyFileData->new(@_);
228             }
229              
230              
231 10751     10751 1 9524 sub write_file_extent($fh, $addr, $size, $data_ref, $ofs=0, $descrip=undef) {
  10751         9477  
  10751         9263  
  10751         9207  
  10751         9357  
  10751         9348  
  10751         9847  
  10751         8806  
232 10751 0 0     16378 $log->tracef("write %s at 0x%X-0x%X from buf size 0x%X%s",
    50          
233             $descrip//'blocks', $addr, $addr+$size, length($$data_ref), $ofs? sprintf(" ofs 0x%X", $ofs) : ''
234             ) if $log->is_trace;
235 10751 50       51586 return unless $size > 0;
236 10751 50       12969 if (defined $addr) {
237 10751   33     40039 my $reached= sysseek($fh, $addr, 0) // croak "sysseek($addr): $!";
238 10751 50       14787 $reached == $addr or croak "sysseek($addr) arrived at $reached instead of $addr";
239             }
240 10751   50     13787 $ofs //= 0;
241 10751 50       13146 my $avail= $data_ref? (length($$data_ref) - $ofs) : 0;
242 10751         8967 my $second;
243             # always write full size, padding with zeroes
244 10751 100       14026 if ($avail < $size) {
245             # If the scalar is particularly large, do two writes instead of reallocating the buffer.
246 1747 50       1975 if ($avail > 0x100000) {
247 0         0 my $first_write= $avail - ($avail & 0xFFF);
248 0         0 $second= pack 'a'.($size-$first_write), substr($$data_ref, $first_write);
249 0         0 $size= $first_write;
250             } else {
251 1747 50       5915 my $data= pack 'a'.$size, ($avail > 0? substr($$data_ref, $ofs) : '');
252 1747         1761 $data_ref= \$data;
253 1747         1768 $ofs= 0;
254             }
255             }
256 10751         180834 my $wrote= syswrite($fh, $$data_ref, $size, $ofs);
257 10751 50       20500 croak "syswrite: $!" if !defined $wrote;
258 10751 50       14248 croak "Unexpected short write ($wrote != $size)" if $wrote != $size;
259 10751 50       13245 if (length $second) {
260 0         0 $wrote= syswrite($fh, $second);
261 0 0       0 croak "syswrite: $!" if !defined $wrote;
262 0 0       0 croak "Unexpected short write ($wrote != $size)" if $wrote != length($second);
263             }
264 10751         16224 return 1;
265             }
266              
267             if (eval { pack('Q<', 1) }) {
268             *_pack= \*CORE::pack;
269             *_unpack= \*CORE::unpack;
270             } else {
271             eval <<'END';
272             # On perl without 64-bit support, replace all 'Q' with 32-bit operations
273             # This does not handle full pack syntax, just what is used in this module collection.
274             sub _pack {
275             my $fmt= shift;
276             my $new_fmt= '';
277             my @new_args;
278             require Math::BigInt;
279             my $mask32= Math::BigInt->new('4294967295');
280             for (split / +/, $fmt) {
281             if ($_ eq 'Q>') {
282             # Convert a 64-bit integer into two 32-bit big-endian arguments
283             $new_fmt .= 'NN';
284             my $qw= Math::BigInt->new(shift);
285             push @new_args, ($qw >> 32)->numify(), ($qw & $mask32)->numify();
286             } elsif ($_ eq 'Q<') {
287             # Convert 64-bit integer into two 32-bit little-endian arguments
288             $new_fmt .= 'VV';
289             my $qw= Math::BigInt->new(shift);
290             push @new_args, ($qw & $mask32)->numify(), ($qw >> 32)->numify();
291             } else {
292             $new_fmt .= $_;
293             push @new_args, shift;
294             }
295             }
296             return pack $new_fmt, @new_args;
297             }
298             # This does not handle full unpack syntax, just what is used in this module collection.
299             sub _unpack {
300             my $fmt= shift;
301             my $new_fmt= '';
302             my @replacements;
303             require Math::BigInt;
304             my @fields= split / +/, $fmt;
305             for (reverse 0 .. $#fields) {
306             if ($fields[$_] eq 'Q>') {
307             $new_fmt .= 'a8';
308             push @replacements, [ $_, sub {
309             my ($h,$l)= unpack('NN', $_[0]);
310             (Math::BigInt->new($h) << 32) | $l
311             }];
312             } elsif ($fields[$_] eq 'Q<') {
313             $new_fmt .= 'a8';
314             push @replacements, [ $_, sub {
315             my ($l, $h)= unpack('VV', $_[0]);
316             (Math::BigInt->new($h) << 32) | $l
317             }];
318             }
319             }
320             my @vals= unpack $new_fmt;
321             for (@replacements) {
322             $vals[$_->[0]]= $_->[1]->($vals[$_->[0]]);
323             }
324             return @vals;
325             }
326             END
327             }
328              
329             1;
330              
331             __END__