File Coverage

lib/SMB/File.pm
Criterion Covered Total %
statement 133 189 70.3
branch 79 134 58.9
condition 20 40 50.0
subroutine 27 42 64.2
pod 26 29 89.6
total 285 434 65.6


line stmt bran cond sub pod time code
1             # SMB-Perl library, Copyright (C) 2014-2018 Mikhael Goikhman, migo@cpan.org
2             #
3             # This program is free software: you can redistribute it and/or modify
4             # it under the terms of the GNU General Public License as published by
5             # the Free Software Foundation, either version 3 of the License, or
6             # (at your option) any later version.
7             #
8             # This program is distributed in the hope that it will be useful,
9             # but WITHOUT ANY WARRANTY; without even the implied warranty of
10             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11             # GNU General Public License for more details.
12             #
13             # You should have received a copy of the GNU General Public License
14             # along with this program. If not, see .
15              
16             package SMB::File;
17              
18 3     3   150104 use strict;
  3         27  
  3         88  
19 3     3   15 use warnings;
  3         7  
  3         102  
20              
21 3     3   977 use parent 'SMB';
  3         670  
  3         16  
22              
23 3     3   1635 use Time::HiRes qw(stat);
  3         4434  
  3         13  
24 3     3   544 use File::Basename qw(basename);
  3         8  
  3         233  
25 3     3   31 use File::Glob qw(bsd_glob GLOB_NOCASE GLOB_BRACE);
  3         16  
  3         313  
26 3     3   21 use Fcntl qw(:mode O_DIRECTORY O_RDONLY O_RDWR O_CREAT O_EXCL O_TRUNC);
  3         7  
  3         1022  
27 3     3   1550 use POSIX qw(strftime);
  3         21600  
  3         22  
28              
29 3     3   6562 use SMB::Time qw(from_nttime to_nttime);
  3         9  
  3         372  
30              
31             use constant {
32 3         924 ATTR_READONLY => 0x00000001,
33             ATTR_HIDDEN => 0x00000002,
34             ATTR_SYSTEM => 0x00000004,
35             ATTR_DIRECTORY => 0x00000010,
36             ATTR_ARCHIVE => 0x00000020,
37             ATTR_DEVICE => 0x00000040,
38             ATTR_NORMAL => 0x00000080,
39             ATTR_TEMPORARY => 0x00000100,
40             ATTR_SPARSE_FILE => 0x00000200,
41             ATTR_REPARSE_POINT => 0x00000400,
42             ATTR_COMPRESSED => 0x00000800,
43             ATTR_OFFLINE => 0x00001000,
44             ATTR_NOT_CONTENT_INDEXED => 0x00002000,
45             ATTR_ENCRYPTED => 0x00004000,
46              
47             DISPOSITION_SUPERSEDE => 0, # exists ? supersede : create
48             DISPOSITION_OPEN => 1, # exists ? open : fail
49             DISPOSITION_CREATE => 2, # exists ? fail : create
50             DISPOSITION_OPEN_IF => 3, # exists ? open : create
51             DISPOSITION_OVERWRITE => 4, # exists ? overwrite : fail
52             DISPOSITION_OVERWRITE_IF => 5, # exists ? overwrite : create
53              
54             ACTION_NONE => -1,
55             ACTION_SUPERSEDED => 0, # existing file was deleted and new file was created in its place
56             ACTION_OPENED => 1, # existing file was opened
57             ACTION_CREATED => 2, # new file was created
58             ACTION_OVERWRITTEN => 3, # new file was overwritten
59 3     3   28 };
  3         7  
60              
61 3     3   954 use SMB::OpenFile;
  3         8  
  3         3889  
62              
63             sub from_ntattr ($) {
64 21   50 21 1 87 my $attr = shift || 0;
65              
66             return
67 21 50       1105 $attr & ATTR_DIRECTORY ? O_DIRECTORY :
    50          
68             $attr & ATTR_READONLY ? O_RDONLY : O_RDWR;
69             }
70              
71             sub to_ntattr ($) {
72 13   50 13 1 23 my $mode = shift || 0;
73              
74 13 100       123 return 0
    100          
    50          
    50          
75             | (S_ISREG($mode) ? ATTR_NORMAL : 0)
76             | (S_ISDIR($mode) ? ATTR_DIRECTORY : 0)
77             # | (S_ISBLK($mode) ? ATTR_DEVICE : 0)
78             | (S_ISCHR($mode) ? ATTR_DEVICE : 0)
79             | ($mode & S_IWUSR ? 0 : ATTR_READONLY)
80             ;
81             }
82              
83             sub new ($%) {
84 14     14 1 219 my $class = shift;
85 14         36 my %options = @_;
86              
87 14   50     32 my $name = delete $options{name} // die "No name in constructor";
88 14         31 $name =~ s!\\!/!g;
89 14         18 $name =~ s!/{2,}!/!g;
90 14         18 $name =~ s!/$!!;
91 14         18 my $is_directory = delete $options{is_directory};
92 14   50     26 my $root = $options{share_root} //= undef;
93 14         18 my $filename = undef;
94 14 50       28 if ($root) {
95 14 50       203 die "No share_root directory ($root)" unless -d $root;
96 14         205 while ($root =~ s=(^|/)(?!\.\./)[^/]+/\.\./=$1=) {}
97 14 100       45 $filename = $name eq '' ? $root : "$root/$name";
98 14         23 $filename =~ s!/{2,}!/!g;
99 14 50       38 $filename = '.' if $filename eq '';
100             }
101 14         18 $name =~ s!/!\\!g;
102 14   50     53 my $is_ipc = $options{is_ipc} ||= 0;
103 14 100 66     350 my @stat = !$is_ipc && $filename && -e $filename ? stat($filename) : ();
104 14   33     49 my $is_svc = $is_ipc && $name =~ /^(?:srvsvc|wkssvc)$/;
105              
106 14 100 100     95 my $self = $class->SUPER::new(
    100 66        
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
107             name => $name,
108             filename => $filename, # server-side file only
109             id => @stat ? $stat[ 1] : 0,
110             creation_time => @stat ? to_nttime($stat[10]) : 0,
111             last_access_time => @stat ? to_nttime($stat[ 8]) : 0,
112             last_write_time => @stat ? to_nttime($stat[ 9]) : 0,
113             change_time => @stat ? to_nttime($stat[ 9]) : 0,
114             allocation_size => @stat ? ($stat[12] || 0) * 512: 0,
115             end_of_file => @stat ? $stat[ 7] : 0,
116             attributes => @stat ? to_ntattr($stat[ 2]) : $is_directory ? ATTR_DIRECTORY : 0,
117             exists => @stat || $is_svc ? 1 : 0,
118             is_svc => $is_svc,
119             opens => 0,
120             %options,
121             );
122              
123 14         53 return $self;
124             }
125              
126             sub update ($$$$$$$$;$) {
127 0     0 1 0 my $self = shift;
128              
129 0         0 $self->creation_time(shift);
130 0         0 $self->last_access_time(shift);
131 0         0 $self->last_write_time(shift);
132 0         0 $self->change_time(shift);
133 0         0 my $size1 = shift;
134 0         0 my $size2 = shift;
135 0         0 $self->attributes(shift);
136 0         0 my $is_eof_first = shift;
137 0 0       0 $self->allocation_size($is_eof_first ? $size2 : $size1);
138 0 0       0 $self->end_of_file ($is_eof_first ? $size1 : $size2);
139              
140 0 0       0 $self->exists($self->creation_time ? 1 : 0);
141             }
142              
143             sub is_directory ($) {
144 13     13 1 19 my $self = shift;
145              
146 13 100       43 return $self->is_ipc ? 0 : $self->attributes & ATTR_DIRECTORY ? 1 : 0;
    50          
147             }
148              
149             sub time_to_string ($;$) {
150 0     0 1 0 my $time = shift;
151 0   0     0 my $format = shift || "%4Y-%2m-%2d %2H:%2M";
152              
153 0         0 return strftime($format, localtime $time);
154             }
155              
156 0     0 1 0 sub ctime { from_nttime($_[0]->creation_time) }
157 0     0 1 0 sub atime { from_nttime($_[0]->last_access_time) }
158 0     0 1 0 sub wtime { from_nttime($_[0]->last_write_time) }
159 0     0 1 0 sub mtime { from_nttime($_[0]->change_time) }
160 0     0 1 0 sub ctime_string { time_to_string($_[0]->ctime, $_[1]) }
161 0     0 1 0 sub atime_string { time_to_string($_[0]->atime, $_[1]) }
162 0     0 1 0 sub wtime_string { time_to_string($_[0]->wtime, $_[1]) }
163 0     0 1 0 sub mtime_string { time_to_string($_[0]->mtime, $_[1]) }
164              
165             use constant {
166 3         6561 KILO => 1024 ** 1,
167             MEGA => 1024 ** 2,
168             GIGA => 1024 ** 3,
169             TERA => 1024 ** 4,
170 3     3   28 };
  3         7  
171              
172 0     0 1 0 sub size { $_[0]->end_of_file }
173             sub size_string {
174 0     0 1 0 my $size = $_[0]->size;
175 0 0       0 $size < 1e10 ? $size :
    0          
    0          
176             $size < 1e13 ? sprintf "%.3f G", $size / GIGA :
177             $size < 1e16 ? sprintf "%.3f T", $size / TERA :
178             substr($size, 0, 6) . "e" . (length($size) - 5);
179             }
180              
181             sub add_openfile ($$$) {
182 17     17 1 36 my $self = shift;
183 17         29 my $handle = shift;
184 17         28 my $action = shift;
185              
186 17         106 my $openfile = SMB::OpenFile->new($self, $handle, $action);
187              
188 17         36 $self->{opens}++;
189 17         57 $self->exists(1);
190              
191 17         49 return $openfile;
192             }
193              
194             sub delete_openfile ($$) {
195 17     17 1 30 my $self = shift;
196 17         28 my $openfile = shift;
197              
198 17 50       64 close($openfile->handle)
199             if $openfile->handle;
200              
201 17         230 --$self->{opens};
202             }
203              
204             sub _fail_exists ($$) {
205 4     4   14 my $self = shift;
206 4         7 my $exists = shift;
207              
208 4         48 $self->err("Can't open file [$self->{filename}]: $!");
209 4   100     31 $self->exists($exists || 0);
210              
211 4         24 return undef;
212             }
213              
214             sub supersede ($) {
215 3     3 1 7 my $self = shift;
216              
217 3 100       9 return $self->create unless -e $self->filename;
218              
219 2         12 my $filename = $self->filename;
220 2         7 my $tmp_filename = sprintf "%s.%06d", $self->filename, rand(1000000);
221              
222 2 50       107 rename($filename, $tmp_filename)
223             or return $self->_fail_exists(1);
224 2         12 my $openfile = $self->create;
225 2 50       8 unless ($openfile) {
226 0 0       0 rename($tmp_filename, $filename)
227             or $self->err("Can't rename tmp file ($tmp_filename) to orig file ($filename)");
228 0         0 return $self->_fail_exists(0);
229             }
230 2 50       96 unlink($tmp_filename)
231             or $self->err("Can't remove tmp file ($tmp_filename)");
232              
233 2         14 $openfile->action(ACTION_SUPERSEDED);
234              
235 2         6 return $openfile;
236             }
237              
238             sub open ($) {
239 8     8 1 20 my $self = shift;
240              
241 8 100       50 sysopen(my $fh, $self->filename, from_ntattr($self->attributes))
242             or return $self->_fail_exists(0);
243              
244 6         32 $self->add_openfile($fh, ACTION_OPENED);
245             }
246              
247             sub create ($) {
248 8     8 1 19 my $self = shift;
249              
250 8 50       25 ($self->is_directory
    50          
    100          
251             ? mkdir($self->filename, 0777) # respect umask
252             : sysopen(my $fh, $self->filename, from_ntattr($self->attributes) | O_CREAT | O_EXCL))
253             or return $self->_fail_exists(-e $self->filename ? 1 : 0);
254              
255 7         45 $self->add_openfile($fh, ACTION_CREATED);
256             }
257              
258             sub overwrite ($) {
259 5     5 1 14 my $self = shift;
260              
261             # no idea why O_TRUNC fails on Windows
262 5 50       23 my $mode = $^O eq 'MSWin32' ? 0 : O_TRUNC;
263              
264 5 100       30 sysopen(my $fh, $self->filename, from_ntattr($self->attributes) | $mode)
265             or return $self->_fail_exists(0);
266              
267 4 50       25 truncate($fh, 0) if $^O eq 'MSWin32';
268              
269 4         17 $self->add_openfile($fh, ACTION_OVERWRITTEN);
270             }
271              
272             sub open_if ($) {
273 4     4 1 8 my $self = shift;
274              
275 4 100       13 return -e $self->filename ? $self->open : $self->create;
276             }
277              
278             sub overwrite_if ($) {
279 3     3 1 8 my $self = shift;
280              
281 3 100       11 return -e $self->filename ? $self->overwrite : $self->create;
282             }
283              
284             sub open_by_disposition ($$) {
285 6     6 1 15 my $self = shift;
286 6         10 my $disposition = shift;
287              
288 6 50       17 return $self->add_openfile(undef, ACTION_OPENED)
289             if $self->is_ipc;
290              
291 6 100       17 return $self->supersede if $disposition == DISPOSITION_SUPERSEDE;
292 5 100       16 return $self->open if $disposition == DISPOSITION_OPEN;
293 4 100       12 return $self->create if $disposition == DISPOSITION_CREATE;
294 3 100       11 return $self->open_if if $disposition == DISPOSITION_OPEN_IF;
295 2 100       7 return $self->overwrite if $disposition == DISPOSITION_OVERWRITE;
296 1 50       7 return $self->overwrite_if if $disposition == DISPOSITION_OVERWRITE_IF;
297              
298 0         0 warn "Invalid disposition $disposition, can not open file\n";
299 0         0 return;
300             }
301              
302             sub normalize_name_in_share ($$) {
303 0     0 0 0 my $self = shift;
304 0   0     0 my $name = shift // die "Missing file name to normalize in share\n";
305              
306 0         0 my $root = $self->share_root;
307 0 0       0 return unless $root;
308              
309 0         0 $name =~ s=\\=\/=g;
310 0         0 $name =~ s=/{2,}=/=g;
311 0         0 $name =~ s=^/|/$==;
312 0         0 $name =~ s=(^|/)\.(/|$)=$1=g;
313 0         0 while ($name =~ s=(^|/)(?!\.\./)[^/]+/\.\./=$1=) {}
314              
315             # refuse to go below the root
316 0 0       0 return if $name =~ m=^\.\.?(/|$)=;
317              
318 0 0       0 return $name eq '' ? $root : "$root/$name";
319             }
320              
321             sub find_files ($%) {
322 5     5 1 443 my $self = shift;
323 5         11 my %params = @_;
324              
325 5 50       10 return unless $self->is_directory;
326              
327 5   100     16 my $pattern = $params{pattern} || '*';
328 5   100     12 my $start_idx = $params{start_idx} || 0;
329 5         10 my $name = $self->name;
330 5         7 $name =~ s=\\=/=g;
331              
332 5   100     14 $self->{files} ||= {};
333             # cached for fragmented queries
334 5 100       9 my $files = $params{reopen} ? undef : $self->{files}{$pattern};
335              
336             # fix pattern if needed
337 5         5 my $pattern0 = $pattern;
338 5         16 $pattern0 =~ s/^\*/{.*,*}/;
339              
340 5 100       9 if (!$files) {
341 4 50       9 my @filenames = map { -e $_ && basename($_) } bsd_glob($self->filename . "/$pattern0", GLOB_NOCASE | GLOB_BRACE);
  12         468  
342             $self->msg("Find $self->{filename}/$pattern - " . scalar(@filenames) . " files")
343 4 50       36 unless $params{quiet};
344 4 50       45 $files = [ map { SMB::File->new(
  12         41  
345             name => $_,
346             share_root => $self->share_root . ($name eq '' ? '' : "/$name"),
347             ) } @filenames ];
348 4         13 $self->{files}{$pattern} = $files;
349             }
350              
351 5 100       21 return $start_idx ? [ @{$files}[$start_idx .. (@$files - 1)] ] : $files;
  1         4  
352             }
353              
354             sub remove ($) {
355 0     0 0   my $self = shift;
356              
357 0 0         if ($self->is_directory) {
358 0           rmdir($self->filename);
359             } else {
360 0           unlink($self->filename);
361             }
362              
363 0           return $! == 0;
364             }
365              
366             sub rename ($$;$) {
367 0     0 0   my $self = shift;
368 0   0       my $new_filename = shift // die "Missing new filename to rename\n";
369 0   0       my $replace = shift // 0;
370              
371 0           my $filename = $self->filename;
372              
373 0 0         return (SMB::STATUS_OBJECT_NAME_NOT_FOUND, "Bad name [$new_filename]")
374             unless $new_filename = $self->normalize_name_in_share($new_filename);
375 0 0         return (SMB::STATUS_NO_SUCH_FILE, "No such file $filename")
376             unless -e $filename;
377 0 0         return (SMB::STATUS_SHARING_VIOLATION, "New name can't be existing directory")
378             if -d $new_filename;
379 0 0 0       return (SMB::STATUS_OBJECT_NAME_COLLISION, "Already exists")
380             if !$replace && -e $new_filename;
381              
382 0 0         return (SMB::STATUS_ACCESS_DENIED, "Failed to rename")
383             unless rename($self->filename, $new_filename);
384              
385 0           return (SMB::STATUS_SUCCESS);
386             }
387              
388             1;
389              
390             __END__