File Coverage

blib/lib/Net/SSH/Any/_Base.pm
Criterion Covered Total %
statement 27 226 11.9
branch 0 118 0.0
condition 0 70 0.0
subroutine 9 38 23.6
pod 0 2 0.0
total 36 454 7.9


line stmt bran cond sub pod time code
1             package Net::SSH::Any::_Base;
2              
3 1     1   3 use strict;
  1         1  
  1         20  
4 1     1   3 use warnings;
  1         1  
  1         15  
5 1     1   2 use Carp;
  1         1  
  1         36  
6              
7 1     1   3 use File::Spec;
  1         0  
  1         16  
8 1     1   3 use Scalar::Util ();
  1         0  
  1         12  
9 1     1   2 use Encode ();
  1         1  
  1         19  
10              
11 1     1   3 use Net::SSH::Any::Constants qw(SSHA_BACKEND_ERROR SSHA_LOCAL_IO_ERROR SSHA_UNIMPLEMENTED_ERROR SSHA_ENCODING_ERROR);
  1         1  
  1         43  
12 1     1   3 use Net::SSH::Any::Util;
  1         1  
  1         2034  
13             our @CARP_NOT = qw(Net::SSH::Any::Util);
14              
15             sub _new {
16 0     0     my ($class, $opts) = @_;
17 0           my $os = delete $opts->{os};
18              
19 0           my (%remote_cmd, %local_cmd, %remote_extra_args, %local_extra_args);
20 0           for (keys %$opts) {
21 0 0         /^remote_(.*)_cmd$/ and $remote_cmd{$1} = $opts->{$_};
22 0 0         /^local_(.*)_cmd$/ and $local_cmd{$1} = $opts->{$_};
23 0 0         /^remote_(.*)_extra_args$/ and $remote_extra_args{$1} = $opts->{$_};
24 0 0         /^local_(.*)_extra_args$/ and $local_extra_args{$1} = $opts->{$_};
25             }
26              
27 0           my $self = { os => $os,
28             error => 0,
29             error_prefix => [],
30             backend_log => [],
31             remote_cmd => \%remote_cmd,
32             local_cmd => \%local_cmd,
33             remote_extra_args => \%remote_extra_args,
34             local_extra_args => \%local_extra_args,
35             };
36              
37 0   0       my $encoding = $self->{encoding} = delete $opts->{encoding} // 'utf8';
38 0   0       $self->{stream_encoding} = delete $opts->{stream_encoding} // $encoding;
39 0   0       $self->{argument_encoding} = delete $opts->{argument_encoding} // $encoding;
40              
41 0           bless $self, $class;
42 0           $self;
43             }
44              
45             sub _log_error_and_reset_backend {
46 0     0     my $self = shift;
47 0           push @{$self->{backend_log}}, "$self->{backend}: [".($self->{error}+0)."] $self->{error}";
  0            
48 0           $self->{error} = 0;
49 0           delete $self->{backend};
50 0           delete $self->{backend_module};
51             ()
52 0           }
53              
54             sub _load_backend_module {
55 0     0     my ($self, $class, $backend, $required_version) = @_;
56 0 0         $backend =~ /^\w+$/ or croak "Bad backend name '$backend' for class '$class'";
57 0           $self->{backend} = $backend;
58 0           my $module = $self->{backend_module} = "${class}::Backend::${backend}";
59              
60 0           local ($@, $SIG{__DIE__});
61 0           my $ok = eval <
62             no strict;
63             no warnings;
64             require $module;
65             1;
66             EOE
67 0 0         if ($ok) {
68 0 0         if ($required_version) {
69 0 0         if ($module->can('_backend_api_version')) {
70 0           my $version = $module->_backend_api_version;
71 0 0         if ($version >= $required_version) {
72 0           return 1;
73             }
74             else {
75 0           $self->_set_error(SSHA_BACKEND_ERROR,
76             "backend API version $version is too old ($required_version required)");
77             }
78             }
79             else {
80 0           $self->_set_error(SSHA_BACKEND_ERROR, 'method _backend_api_version missing');
81             }
82             }
83             else {
84 0           return 1;
85             }
86             }
87             else {
88 0           $self->_set_error(SSHA_BACKEND_ERROR, "unable to load module '$module'", $@);
89             }
90              
91 0           $self->_log_error_and_reset_backend;
92             ()
93 0           }
94              
95 0     0 0   sub error { shift->{error} }
96              
97             sub die_on_error {
98 0     0 0   my $self = shift;
99 0 0         $self->{error} and croak(join(': ', @_, "$self->{error}"));
100 0           1;
101             }
102              
103             sub _set_error {
104 0     0     my $self = shift;
105 0   0       my $code = shift || 0;
106 0 0         my @msg = grep { defined && length } @_;
  0            
107 0 0         @msg = "Unknown error $code" unless @msg;
108             my $error = $self->{error} = ( $code
109 0 0         ? Scalar::Util::dualvar($code, join(': ', @{$self->{error_prefix}}, @msg))
  0            
110             : 0 );
111 0 0 0       $debug and $debug & 1 and _debug "set_error($code - $error)";
112 0           return $error
113             }
114              
115             sub _or_set_error {
116 0     0     my $self = shift;
117 0 0         $self->{error} or $self->_set_error(@_);
118             }
119              
120             sub _or_check_error_after_eval {
121 0 0   0     if ($@) {
122 0           my ($any, $code) = @_;
123 0 0         unless ($any->{error}) {
124 0           my $err = $@;
125 0           $err =~ s/(.*) at .* line \d+.$/$1/;
126 0           $any->_set_error($code, $err);
127             }
128 0           return 0;
129             }
130             1
131 0           }
132              
133             sub _open_file {
134 0     0     my ($self, $def_mode, $name_or_args) = @_;
135 0 0         my ($mode, @args) = (ref $name_or_args
136             ? @$name_or_args
137             : ($def_mode, $name_or_args));
138 0 0         if (open my $fh, $mode, @args) {
139 0           return $fh;
140             }
141 0           $self->_set_error(SSHA_LOCAL_IO_ERROR, "Unable to open file '@args': $!");
142 0           return undef;
143             }
144              
145             my %loaded;
146             sub _load_module {
147 0     0     my ($self, $module) = @_;
148 0 0 0       $loaded{$module} ||= eval "require $module; 1" and return 1;
149 0           $self->_set_error(SSHA_UNIMPLEMENTED_ERROR, "Unable to load perl module $module");
150 0           return;
151             }
152              
153             sub _load_os {
154 0     0     my $self = shift;
155 0 0 0       my $os = $self->{os} //= ($^O =~ /^mswin/i ? 'MSWin' : 'POSIX');
156 0           my $os_module = "Net::SSH::Any::OS::$os";
157 0 0         $self->_load_module($os_module) or return;
158 0           $self->{os_module} = $os_module;
159             }
160              
161             sub _find_cmd_by_friend {
162 0     0     my ($any, $name, $friend) = @_;
163 0 0         if (defined $friend) {
164 0           my $up = File::Spec->updir;
165 0           my ($drive, $dir) = File::Spec->splitpath($friend);
166 0           my $base = File::Spec->catpath($drive, $dir);
167 0           for my $path (File::Spec->join($base, $name),
168             map File::Spec->join($base, $up, $_, $name), qw(bin sbin libexec) ) {
169 0           my $cmd = $any->_os_validate_cmd($path);
170 0 0         return $cmd if defined $cmd;
171             }
172             }
173             ()
174 0           }
175              
176             sub _find_cmd_in_path {
177 0     0     my ($any, $name) = @_;
178 0           for my $path (File::Spec->path) {
179 0           my $cmd = $any->_os_validate_cmd(File::Spec->join($path, $name));
180 0 0         return $cmd if defined $cmd;
181             }
182             ()
183 0           }
184              
185             sub _find_cmd {
186 0     0     my ($any, $name, $friend, $app, $default) = @_;
187 0           my $safe_name = $name;
188 0           $safe_name =~ s/\W/_/g;
189 0   0       return ( $any->{local_cmd}{$safe_name} //
      0        
      0        
      0        
      0        
      0        
190             $any->_find_cmd_by_friend($name, $friend) //
191             $any->_find_cmd_in_path($name) //
192             $any->_find_helper_cmd($name) //
193             $any->_os_find_cmd_by_app($name, $app) //
194             $any->_os_validate_cmd($default) //
195             $name );
196             }
197              
198             sub _find_helper_cmd {
199 0     0     my ($any, $name) = @_;
200 0 0 0       $debug and $debug & 1024 and _debug "looking for helper $name";
201 0   0       my $module = my $last = $any->{backend_module} // return;
202 0 0         $last =~ s/.*::// or return;
203 0           $module =~ s{::}{/}g;
204 0 0 0       $debug and $debug & 1024 and _debug "module as \$INC key is ", $module, ".pm";
205 0   0       my $file_pm = $INC{"$module.pm"} // return;
206 0           my ($drive, $dir) = File::Spec->splitpath(File::Spec->rel2abs($file_pm));
207 0           my $path = File::Spec->catpath($drive, $dir, $last, 'Helpers', $name);
208 0           $any->_os_validate_cmd($path);
209             }
210              
211             sub _find_local_extra_args {
212 0     0     my ($any, $name, $opts, @default) = @_;
213 0           my $safe_name = $name;
214 0           $safe_name =~ s/\W/_/g;
215             my $extra = ( $opts->{"local_${safe_name}_extra_args"} //
216 0   0       $any->{local_extra_args}{$safe_name} //
      0        
217             \@default );
218 0           [_array_or_scalar_to_list $extra]
219             }
220              
221             my %posix_shell = map { $_ => 1 } qw(POSIX bash sh ksh ash dash pdksh mksh lksh zsh fizsh posh);
222              
223             sub _new_quoter {
224 0     0     my ($any, $shell) = @_;
225 0 0         if ($posix_shell{$shell}) {
226 0 0         $any->_load_module('Net::SSH::Any::POSIXShellQuoter') or return;
227 0           return 'Net::SSH::Any::POSIXShellQuoter';
228             }
229             else {
230 0 0         $any->_load_module('Net::OpenSSH::ShellQuoter') or return;
231 0           return Net::OpenSSH::ShellQuoter->quoter($shell);
232             }
233             }
234              
235             sub _quoter {
236 0     0     my ($any, $shell) = @_;
237 0 0         defined $shell or croak "shell argument is undef";
238 0           return $any->_new_quoter($shell);
239             }
240              
241             sub _quote_args {
242 0     0     my $any = shift;
243 0           my $opts = shift;
244 0 0         ref $opts eq 'HASH' or die "internal error";
245 0           my $quote = delete $opts->{quote_args};
246 0           my $glob_quoting = delete $opts->{glob_quoting};
247 0           my $argument_encoding = $any->_delete_argument_encoding($opts);
248 0 0         $quote = (@_ > 1) unless defined $quote;
249              
250 0           my @quoted;
251 0 0         if ($quote) {
252 0   0       my $shell = delete $opts->{remote_shell} // delete $opts->{shell};
253 0 0         my $quoter = $any->_quoter($shell) or return;
254 0 0         my $quote_method = ($glob_quoting ? 'quote_glob' : 'quote');
255              
256             # foo => $quoter
257             # \foo => $quoter_glob
258             # \\foo => no quoting at all and disable extended quoting as it is not safe
259 0           for (@_) {
260 0 0         if (ref $_) {
261 0 0 0       if (ref $_ eq 'SCALAR') {
    0          
262 0           push @quoted, $quoter->quote_glob($$_);
263             }
264             elsif (ref $_ eq 'REF' and ref $$_ eq 'SCALAR') {
265 0           push @quoted, $$$_;
266             }
267             else {
268 0           croak "invalid reference in remote command argument list"
269             }
270             }
271             else {
272 0           push @quoted, $quoter->$quote_method($_);
273             }
274             }
275             }
276             else {
277 0 0         croak "reference found in argument list when argument quoting is disabled" if (grep ref, @_);
278 0           @quoted = @_;
279             }
280 0           $any->_encode_args($argument_encoding, @quoted);
281 0 0 0       $debug and $debug & 1024 and _debug("command+args: @quoted");
282 0 0         wantarray ? @quoted : join(" ", @quoted);
283             }
284              
285             sub _delete_argument_encoding {
286 0     0     my ($any, $opts) = @_;
287             _first_defined(delete $opts->{argument_encoding},
288             delete $opts->{encoding},
289             $any->{argument_encoding})
290 0           }
291              
292             sub _delete_stream_encoding {
293 0     0     my ($any, $opts) = @_;
294             _first_defined(delete $opts->{stream_encoding},
295             $opts->{encoding},
296             $any->{stream_encoding})
297 0           }
298              
299             sub _find_encoding {
300 0     0     my ($any, $encoding, $data) = @_;
301 0 0         my $enc = Encode::find_encoding($encoding)
302             or $any->_or_set_error(SSHA_ENCODING_ERROR, "bad encoding '$encoding'");
303 0           return $enc
304             }
305              
306             sub _encode_data {
307 0     0     my $any = shift;
308 0           my $encoding = shift;
309 0 0         if (@_) {
310 0 0         my $enc = $any->_find_encoding($encoding) or return;
311 0           local $any->{error_prefix} = [@{$any->{error_prefix}}, "data encoding failed"];
  0            
312 0           local ($@, $SIG{__DIE__});
313 0   0       eval { defined and $_ = $enc->encode($_, Encode::FB_CROAK()) for @_ };
  0            
314 0 0         $any->_or_check_error_after_eval(SSHA_ENCODING_ERROR) or return;
315             }
316             1
317 0           }
318              
319             sub _decode_data {
320 0     0     my $any = shift;
321 0           my $encoding = shift;
322 0 0         my $enc = $any->_find_encoding($encoding) or return;
323 0 0         if (@_) {
324 0           local ($@, $SIG{__DIE__});
325 0   0       eval { defined and $_ = $enc->decode($_, Encode::FB_CROAK()) for @_ };
  0            
326 0 0         $any->_or_check_error_after_eval(SSHA_ENCODING_ERROR) or return;
327             }
328 0           1;
329             }
330              
331             sub _encode_args {
332 0 0   0     if (@_ > 2) {
333 0           my $any = shift;
334 0           my $encoding = shift;
335 0           local $any->{error_prefix} = [@{$any->{error_prefix}}, "argument encoding failed"];
  0            
336 0 0         if (my $enc = $any->_find_encoding($encoding)) {
337 0           $any->_encode_data($enc, @_);
338             }
339 0           return !$any->{_error};
340             }
341 0           1;
342             }
343              
344             # transparently delegate method calls to backend and os packages:
345             sub AUTOLOAD {
346 0     0     our $AUTOLOAD;
347 0           my ($name) = $AUTOLOAD =~ /([^:]*)$/;
348 0           my $sub;
349 1     1   5 no strict 'refs';
  1         1  
  1         217  
350 0 0         if (my ($os_name) = $name =~ /^_os_(.*)/) {
351             $sub = sub {
352 0 0 0 0     my $os = $_[0]->{os_module} //= $_[0]->_load_os or return;
353 0 0         my $method = $os->can($os_name)
354             or croak "method '$os_name' not defined in OS '$os'";
355 0           goto &$method;
356 0           };
357             }
358             else {
359             $sub = sub {
360 0 0   0     my $module = $_[0]->{backend_module} or return;
361 0 0         my $method = $module->can($name)
362             or croak "method '$name' not defined in backend '$module'";
363 0           goto &$method;
364 0           };
365             }
366 0           *{$AUTOLOAD} = $sub;
  0            
367 0           goto &$sub;
368             }
369              
370             sub DESTROY {
371 0     0     my $self = shift;
372 0           my $module = $self->{backend_module};
373 0 0         if (defined $module) {
374 0           my $sub = $module->can('DESTROY');
375 0 0         $sub->($self) if $sub;
376             }
377             }
378              
379             1;