File Coverage

blib/lib/Sys/Export.pm
Criterion Covered Total %
statement 132 162 81.4
branch 59 102 57.8
condition 17 37 45.9
subroutine 31 39 79.4
pod 26 28 92.8
total 265 368 72.0


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