File Coverage

lib/SMB/File.pm
Criterion Covered Total %
statement 128 181 70.7
branch 74 122 60.6
condition 21 41 51.2
subroutine 26 39 66.6
pod 24 27 88.8
total 273 410 66.5


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   129403 use strict;
  3         20  
  3         65  
19 3     3   10 use warnings;
  3         5  
  3         63  
20              
21 3     3   666 use parent 'SMB';
  3         506  
  3         12  
22              
23 3     3   1255 use Time::HiRes qw(stat);
  3         3136  
  3         9  
24 3     3   384 use File::Basename qw(basename);
  3         4  
  3         156  
25 3     3   16 use File::Glob qw(bsd_glob GLOB_NOCASE GLOB_BRACE);
  3         11  
  3         244  
26 3     3   37 use Fcntl qw(:mode O_DIRECTORY O_RDONLY O_RDWR O_CREAT O_EXCL O_TRUNC);
  3         6  
  3         756  
27 3     3   1167 use POSIX qw(strftime);
  3         14526  
  3         22  
28              
29 3     3   3848 use SMB::Time qw(from_nttime to_nttime);
  3         6  
  3         230  
30              
31             use constant {
32 3         500 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   15 };
  3         5  
60              
61 3     3   636 use SMB::OpenFile;
  3         17  
  3         6106  
62              
63             sub from_ntattr ($) {
64 21   50 21 1 65 my $attr = shift || 0;
65              
66             return
67 21 50       864 $attr & ATTR_DIRECTORY ? O_DIRECTORY :
    50          
68             $attr & ATTR_READONLY ? O_RDONLY : O_RDWR;
69             }
70              
71             sub to_ntattr ($) {
72 11   50 11 1 22 my $mode = shift || 0;
73              
74 11 100       99 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 12     12 1 243 my $class = shift;
85 12         45 my %options = @_;
86              
87 12   50     37 my $name = delete $options{name} // die "No name in constructor";
88 12         29 $name =~ s!\\!\/!g;
89 12         17 $name =~ s!/{2,}!/!g;
90 12         16 $name =~ s!/$!!;
91 12         18 my $is_directory = delete $options{is_directory};
92 12   50     26 my $root = $options{share_root} //= undef;
93 12         17 my $filename = undef;
94 12 50       27 if ($root) {
95 12 50       179 die "No share_root directory ($root)" unless -d $root;
96 12         171 while ($root =~ s=(^|/)(?!\.\./)[^/]+/\.\./=$1=) {}
97 12 100       44 $filename = $name eq '' ? $root : "$root/$name";
98 12         24 $filename =~ s!/{2,}!/!g;
99 12 50       25 $filename = '.' if $filename eq '';
100             }
101 12   50     48 my $is_ipc = $options{is_ipc} ||= 0;
102 12 100 66     302 my @stat = !$is_ipc && $filename && -e $filename ? stat($filename) : ();
103 12   33     43 my $is_srv = $is_ipc && $name =~ /^(?:srvsvc|wkssvc)$/;
104              
105 12 100 100     73 my $self = $class->SUPER::new(
    100 66        
    100          
    100          
    100          
    100          
    50          
    100          
    100          
106             name => $name,
107             filename => $filename, # server-side file only
108             creation_time => @stat ? to_nttime($stat[10]) : 0,
109             last_access_time => @stat ? to_nttime($stat[ 8]) : 0,
110             last_write_time => @stat ? to_nttime($stat[ 9]) : 0,
111             change_time => @stat ? to_nttime($stat[ 9]) : 0,
112             allocation_size => @stat ? ($stat[12] || 0) * 512: 0,
113             end_of_file => @stat ? $stat[ 7] : 0,
114             attributes => @stat ? to_ntattr($stat[ 2]) : $is_directory ? ATTR_DIRECTORY : 0,
115             exists => @stat || $is_srv ? 1 : 0,
116             opens => 0,
117             %options,
118             );
119              
120 12         41 return $self;
121             }
122              
123             sub update ($$$$$$$$;$) {
124 0     0 1 0 my $self = shift;
125              
126 0         0 $self->creation_time(shift);
127 0         0 $self->last_access_time(shift);
128 0         0 $self->last_write_time(shift);
129 0         0 $self->change_time(shift);
130 0         0 my $size1 = shift;
131 0         0 my $size2 = shift;
132 0         0 $self->attributes(shift);
133 0         0 my $is_eof_first = shift;
134 0 0       0 $self->allocation_size($is_eof_first ? $size2 : $size1);
135 0 0       0 $self->end_of_file ($is_eof_first ? $size1 : $size2);
136              
137 0 0       0 $self->exists($self->creation_time ? 1 : 0);
138             }
139              
140             sub is_directory ($) {
141 4     4 1 9 my $self = shift;
142              
143 4 50       20 return $self->is_ipc ? 0 : $self->attributes & ATTR_DIRECTORY ? 1 : 0;
    50          
144             }
145              
146             sub time_to_string ($;$) {
147 0     0 1 0 my $time = shift;
148 0   0     0 my $format = shift || "%4Y-%2m-%2d %2H:%2M";
149              
150 0         0 return strftime($format, localtime $time);
151             }
152              
153 0     0 1 0 sub ctime { from_nttime($_[0]->creation_time) }
154 0     0 1 0 sub atime { from_nttime($_[0]->last_access_time) }
155 0     0 1 0 sub wtime { from_nttime($_[0]->last_write_time) }
156 0     0 1 0 sub mtime { from_nttime($_[0]->change_time) }
157 0     0 1 0 sub ctime_string { time_to_string($_[0]->ctime, $_[1]) }
158 0     0 1 0 sub atime_string { time_to_string($_[0]->atime, $_[1]) }
159 0     0 1 0 sub wtime_string { time_to_string($_[0]->wtime, $_[1]) }
160 0     0 1 0 sub mtime_string { time_to_string($_[0]->mtime, $_[1]) }
161              
162             sub add_openfile ($$$) {
163 17     17 1 23 my $self = shift;
164 17         19 my $handle = shift;
165 17         20 my $action = shift;
166              
167 17         80 my $openfile = SMB::OpenFile->new($self, $handle, $action);
168              
169 17         27 $self->{opens}++;
170 17         39 $self->exists(1);
171              
172 17         35 return $openfile;
173             }
174              
175             sub delete_openfile ($$) {
176 17     17 1 20 my $self = shift;
177 17         19 my $openfile = shift;
178              
179 17 50       63 close($openfile->handle)
180             if $openfile->handle;
181              
182 17         149 --$self->{opens};
183             }
184              
185             sub _fail_exists ($$) {
186 4     4   10 my $self = shift;
187 4         7 my $exists = shift;
188              
189 4         34 $self->err("Can't open file [$self->{filename}]: $!");
190 4   100     27 $self->exists($exists || 0);
191              
192 4         20 return undef;
193             }
194              
195             sub supersede ($) {
196 3     3 1 4 my $self = shift;
197              
198 3 100       6 return $self->create unless -e $self->filename;
199              
200 2         7 my $filename = $self->filename;
201 2         6 my $tmp_filename = sprintf "%s.%06d", $self->filename, rand(1000000);
202              
203 2 50       68 rename($filename, $tmp_filename)
204             or return $self->_fail_exists(1);
205 2         6 my $openfile = $self->create;
206 2 50       5 unless ($openfile) {
207 0 0       0 rename($tmp_filename, $filename)
208             or $self->err("Can't rename tmp file ($tmp_filename) to orig file ($filename)");
209 0         0 return $self->_fail_exists(0);
210             }
211 2 50       81 unlink($tmp_filename)
212             or $self->err("Can't remove tmp file ($tmp_filename)");
213              
214 2         8 $openfile->action(ACTION_SUPERSEDED);
215              
216 2         4 return $openfile;
217             }
218              
219             sub open ($) {
220 8     8 1 15 my $self = shift;
221              
222 8 100       31 sysopen(my $fh, $self->filename, from_ntattr($self->attributes))
223             or return $self->_fail_exists(0);
224              
225 6         25 $self->add_openfile($fh, ACTION_OPENED);
226             }
227              
228             sub create ($) {
229 8     8 1 15 my $self = shift;
230              
231 8 50       38 sysopen(my $fh, $self->filename, from_ntattr($self->attributes) | O_CREAT | O_EXCL)
    100          
232             or return $self->_fail_exists(-e $self->filename ? 1 : 0);
233              
234 7         34 $self->add_openfile($fh, ACTION_CREATED);
235             }
236              
237             sub overwrite ($) {
238 5     5 1 7 my $self = shift;
239              
240             # no idea why O_TRUNC fails on Windows
241 5 50       14 my $mode = $^O eq 'MSWin32' ? 0 : O_TRUNC;
242              
243 5 100       21 sysopen(my $fh, $self->filename, from_ntattr($self->attributes) | $mode)
244             or return $self->_fail_exists(0);
245              
246 4 50       21 truncate($fh, 0) if $^O eq 'MSWin32';
247              
248 4         10 $self->add_openfile($fh, ACTION_OVERWRITTEN);
249             }
250              
251             sub open_if ($) {
252 4     4 1 5 my $self = shift;
253              
254 4 100       8 return -e $self->filename ? $self->open : $self->create;
255             }
256              
257             sub overwrite_if ($) {
258 3     3 1 5 my $self = shift;
259              
260 3 100       6 return -e $self->filename ? $self->overwrite : $self->create;
261             }
262              
263             sub open_by_disposition ($$) {
264 6     6 1 8 my $self = shift;
265 6         7 my $disposition = shift;
266              
267 6 50       15 return $self->add_openfile(undef, ACTION_OPENED)
268             if $self->is_ipc;
269              
270 6 100       22 return $self->supersede if $disposition == DISPOSITION_SUPERSEDE;
271 5 100       21 return $self->open if $disposition == DISPOSITION_OPEN;
272 4 100       9 return $self->create if $disposition == DISPOSITION_CREATE;
273 3 100       7 return $self->open_if if $disposition == DISPOSITION_OPEN_IF;
274 2 100       6 return $self->overwrite if $disposition == DISPOSITION_OVERWRITE;
275 1 50       5 return $self->overwrite_if if $disposition == DISPOSITION_OVERWRITE_IF;
276              
277 0         0 warn "Invalid disposition $disposition, can not open file\n";
278 0         0 return;
279             }
280              
281             sub normalize_name_in_share ($$) {
282 0     0 0 0 my $self = shift;
283 0   0     0 my $name = shift // die "Missing file name to normalize in share\n";
284              
285 0         0 my $root = $self->share_root;
286 0 0       0 return unless $root;
287              
288 0         0 $name =~ s=\\=\/=g;
289 0         0 $name =~ s=/{2,}=/=g;
290 0         0 $name =~ s=^/|/$==;
291 0         0 $name =~ s=(^|/)\.(/|$)=$1=g;
292 0         0 while ($name =~ s=(^|/)(?!\.\./)[^/]+/\.\./=$1=) {}
293              
294             # refuse to go below the root
295 0 0       0 return if $name =~ m=^\.\.?(/|$)=;
296              
297 0 0       0 return $name eq '' ? $root : "$root/$name";
298             }
299              
300             sub find_files ($%) {
301 4     4 1 1871 my $self = shift;
302 4         15 my %params = @_;
303              
304 4 50       13 return unless $self->is_directory;
305              
306 4   100     15 my $pattern = $params{pattern} || '*';
307 4         8 my $want_all = $pattern eq '*';
308 4   100     13 my $start_idx = $params{start_idx} || 0;
309 4         6 my $files = $self->{files}; # cached for fragmented queries
310 4         12 my $name = $self->name;
311              
312             # fix pattern if needed
313 4         8 my $pattern0 = $pattern;
314 4         14 $pattern0 =~ s/^\*/{.*,*}/;
315              
316 4 100 100     18 if (!$files || $start_idx == 0) {
317 3 50       8 my @filenames = map { -e $_ && basename($_) } bsd_glob($self->filename . "/$pattern0", GLOB_NOCASE | GLOB_BRACE);
  10         369  
318 3         26 $self->msg("Find [$self->{filename}/$pattern] - " . scalar(@filenames) . " files");
319 3 50       13 $files = [ map { SMB::File->new(
  10         39  
320             name => $name eq '' ? $_ : "$name/$_",
321             share_root => $self->share_root,
322             ) } @filenames ];
323 3 100       16 $self->{files} = $files if $want_all;
324             }
325              
326 4 100       17 return $start_idx ? [ @{$files}[$start_idx .. (@$files - 1)] ] : $files;
  1         25  
327             }
328              
329             sub remove ($) {
330 0     0 0   my $self = shift;
331              
332 0 0         if ($self->is_directory) {
333 0           rmdir($self->filename);
334             } else {
335 0           unlink($self->filename);
336             }
337              
338 0           return $! == 0;
339             }
340              
341             sub rename ($$;$) {
342 0     0 0   my $self = shift;
343 0   0       my $new_filename = shift // die "Missing new filename to rename\n";
344 0   0       my $replace = shift // 0;
345              
346 0           my $filename = $self->filename;
347              
348 0 0         return (SMB::STATUS_OBJECT_NAME_NOT_FOUND, "Bad name [$new_filename]")
349             unless $new_filename = $self->normalize_name_in_share($new_filename);
350 0 0         return (SMB::STATUS_NO_SUCH_FILE, "No such file $filename")
351             unless -e $filename;
352 0 0         return (SMB::STATUS_SHARING_VIOLATION, "New name can't be existing directory")
353             if -d $new_filename;
354 0 0 0       return (SMB::STATUS_OBJECT_NAME_COLLISION, "Already exists")
355             if !$replace && -e $new_filename;
356              
357 0 0         return (SMB::STATUS_ACCESS_DENIED, "Failed to rename")
358             unless rename($self->filename, $new_filename);
359              
360 0           return (SMB::STATUS_SUCCESS);
361             }
362              
363             1;
364              
365             __END__