File Coverage

blib/lib/Net/SSH/Any/_Base.pm
Criterion Covered Total %
statement 27 233 11.5
branch 0 124 0.0
condition 0 70 0.0
subroutine 9 38 23.6
pod 0 2 0.0
total 36 467 7.7


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