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'; # VERSION
4             # ABSTRACT: Export a subset of an OS file tree, for chroot/initrd
5              
6 18     18   1224306 use v5.26;
  18         56  
7 18     18   67 use warnings;
  18         26  
  18         831  
8 18     18   77 use experimental qw( signatures );
  18         25  
  18         108  
9 18     18   2231 use Carp;
  18         31  
  18         1224  
10 18     18   93 use Scalar::Util qw( blessed looks_like_number );
  18         28  
  18         862  
11 18     18   7607 use Sys::Export::LogAny '$log';
  18         54  
  18         190  
12 18     18   3845 use Exporter ();
  18         30  
  18         2841  
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 18     18   120 require Fcntl;
17 306 100       6915 eval { Fcntl->can($_)->($_ =~ /_IS/? (0) : ()); 1 }? Fcntl->import($_) : eval "sub $_ { 0 }"
  270         62725  
18 18 100   5 0 52 for qw( S_ISREG S_ISDIR S_ISLNK S_ISBLK S_ISCHR S_ISFIFO S_ISSOCK S_ISWHT
  5     0 0 104  
  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
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 41     41   124 my $class= $_[0];
50 41         85 my $caller= caller;
51 41         65 my %ctor_opts;
52 41         169 for (my $i= 1; $i < $#_; ++$i) {
53 111 50       353 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         11 $ctor_opts{$1}= (splice @_, $i--, 2)[1];
58             }
59             }
60 41 100       89 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       8 push @_, 'exporter', ':basic_methods'
65             unless grep /^(add|:.*methods)\z/, @_;
66             }
67 41         190967 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 2 sub init_global_exporter(%config) {
  1         2  
  1         1  
78 1   33     3 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       5 $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       45 eval { $require_module->($class) } or croak "$@";
  1         3  
86             # now construct one
87 1         8 $exporter= $class->new(%config);
88             }
89              
90              
91 3     3 1 16 sub add { $exporter->add(@_) }
92 1     1 1 11 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 10636     10636 1 25409 sub isa_hash :prototype($) { ref $_[0] eq 'HASH' }
102 21099     21099 1 55733 sub isa_array :prototype($) { ref $_[0] eq 'ARRAY' }
103 35 50 33 35 1 493 sub isa_handle :prototype($) { ref $_[0] eq 'GLOB' || (ref $_[0] && (ref $_[0])->isa('IO::Handle')) }
104 31861 100   31861 1 106654 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 9 100 66 9 1 163 sub isa_export_dst :prototype($) { blessed($_[0]) && $_[0]->can('add') && $_[0]->can('finish') }
107 11 100 66 11 1 64 sub isa_userdb :prototype($) { blessed($_[0]) && $_[0]->can('user') && $_[0]->can('group') }
108 18 100   18 1 89 sub isa_user :prototype($) { blessed($_[0]) && $_[0]->isa('Sys::Export::Unix::UserDB::User') }
109 18 100   18 1 67 sub isa_group :prototype($) { blessed($_[0]) && $_[0]->isa('Sys::Export::Unix::UserDB::Group') }
110 1330     1330 1 4221 sub isa_pow2 :prototype($) { !($_[0] & ($_[0]-1)) }
111 24 50 0 24 1 64 sub isa_data_ref :prototype($) { ref $_[0] eq 'SCALAR' || blessed($_[0]) && $_[0]->can('as_scalarref') }
112              
113              
114 4     4   7 sub _parse_major_minor_data($attrs, $data) {
  4         5  
  4         4  
  4         4  
115 4 100       5 @{$attrs}{'rdev_major','rdev_minor'}= isa_array $data? @$data : split(/[,:]/, $data);
  4         9  
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 110210 @_= @{$_[0]} if @_ == 1 && isa_array $_[0];
  10534         20546  
132 10543 100 100     24129 my %attrs= @_ > 2 && isa_hash $_[-1]? %{ pop @_ } : ();
  46         125  
133 10543         15294 my ($mode, $name, $data)= @_;
134 10543         10959 my $mode_desc;
135 10543 50       14778 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       34822 $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       26906 $mode_desc= $_mode_alias{$1}
143             or croak "Unknown mode alias '$1'";
144 10543 100       22342 $mode= $mode_desc->[0] | (defined $2? oct($2) : $mode_desc->[1]->());
145             }
146 10543         17551 $attrs{mode}= $mode;
147 10543 50       15280 length $name or croak "Name must be nonzero length";
148 10543         13390 $attrs{name}= $name;
149 10543 100       15117 if (defined $data) {
150 10533 100 66     22259 if ($mode_desc && $mode_desc->[2]) {
151 4         24 $mode_desc->[2]->(\%attrs, $data);
152             } else {
153 10529         12891 $attrs{data}= $data;
154             }
155             }
156 10543         47240 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 61 sub round_up_to_multiple($n, $pow2) {
  62         53  
  62         56  
  62         54  
174 62 50       129 croak "Not defined for negative numbers" unless $n > 0;
175 62         67 my $mask= $pow2-1;
176 62         121 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 7     7   11 sub _load_file($filename, $offset= 0, $length= undef) {
  7         11  
  7         10  
  7         7  
  7         40  
195 7 50       268 open my $fh, "<:raw", $filename
196             or die "open($filename): $!";
197 7         116 my $size= -s $fh;
198 7 50       35 croak "Offset beyond end of file ($filename, $offset > $size)" if $offset > $size;
199 7   33     21 $length //= $size - $offset;
200 7         14 my $buf= '';
201 7 50       18 if ($length) {
202 7 50       14 if ($offset > 0) {
203 0 0       0 sysseek($fh, $offset, 0) == $offset
204             or croak "sysseek($filename, $offset): $!";
205             }
206 7 50       309 sysread($fh, $buf, $length) == $size
207             or die "sysread($filename, $size): $!";
208             }
209 7         200 \$buf;
210             }
211              
212              
213             sub filedata {
214 7     7 1 77865 state $loaded= require Sys::Export::LazyFileData;
215 7         33 Sys::Export::LazyFileData->new(@_);
216             }
217              
218              
219 10751     10751 1 9726 sub write_file_extent($fh, $addr, $size, $data_ref, $ofs=0, $descrip=undef) {
  10751         9709  
  10751         9684  
  10751         9763  
  10751         9651  
  10751         9556  
  10751         11103  
  10751         9645  
220 10751 0 0     16805 $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       55854 return unless $size > 0;
224 10751 50       13419 if (defined $addr) {
225 10751   33     59854 my $reached= sysseek($fh, $addr, 0) // croak "sysseek($addr): $!";
226 10751 50       15239 $reached == $addr or croak "sysseek($addr) arrived at $reached instead of $addr";
227             }
228 10751   50     13148 $ofs //= 0;
229 10751 50       14543 my $avail= $data_ref? (length($$data_ref) - $ofs) : 0;
230 10751         9188 my $second;
231             # always write full size, padding with zeroes
232 10751 100       13806 if ($avail < $size) {
233             # If the scalar is particularly large, do two writes instead of reallocating the buffer.
234 1747 50       2342 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       5785 my $data= pack 'a'.$size, ($avail > 0? substr($$data_ref, $ofs) : '');
240 1747         1688 $data_ref= \$data;
241 1747         1836 $ofs= 0;
242             }
243             }
244 10751         184659 my $wrote= syswrite($fh, $$data_ref, $size, $ofs);
245 10751 50       18830 croak "syswrite: $!" if !defined $wrote;
246 10751 50       15490 croak "Unexpected short write ($wrote != $size)" if $wrote != $size;
247 10751 50       13121 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         18918 return 1;
253             }
254              
255              
256             1;
257              
258             __END__