Ticket #2910: enter into symlink to commpressed patch shows empty patch.
[midnight-commander.git] / src / vfs / extfs / helpers / patchfs.in
blobc1c4f9959c341246f50bf1f7768aca9ed08fe169
1 #! @PERL@ -w
3 # Written by Adam Byrtek <alpha@debian.org>, 2002
4 # Rewritten by David Sterba <dave@jikos.cz>, 2009
6 # Extfs to handle patches in context and unified diff format.
7 # Known issues: When name of file to patch is modified during editing, 
8 # hunk is duplicated on copyin. It is unavoidable.
10 use bytes;
11 use strict;
12 use POSIX;
13 use File::Temp 'tempfile';
15 # standard binaries
16 my $lzma = 'lzma';
17 my $xz   = 'xz';
18 my $bzip = 'bzip2';
19 my $gzip = 'gzip';
20 my $fileutil = 'file -b';
22 # date parsing requires Date::Parse from TimeDate module
23 my $parsedates = eval 'require Date::Parse';
25 # regular expressions
26 my $unified_header=qr/^--- .*\t.*\n\+\+\+ .*\t.*\n$/;
27 my $unified_extract=qr/^--- ([^\t]+).*\n\+\+\+ ([^\t]+)\s*(.*)\n/;
28 my $unified_header2=qr/^--- .*\n\+\+\+ .*\n$/;
29 my $unified_extract2=qr/^--- ([^\s]+).*\n\+\+\+ ([^\s]+)\s*(.*)\n/;
30 my $unified_contents=qr/^([+\-\\ \n]|@@ .* @@)/;
31 my $unified_hunk=qr/@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*\n/;
33 my $context_header=qr/^\*\*\* .*\t.*\n--- .*\t.*\n$/;
34 my $context_extract=qr/^\*\*\* ([^\t]+).*\n--- ([^\t]+)\s*(.*)\n/;
35 my $context_header2=qr/^\*\*\* .*\n--- .*\n$/;
36 my $context_extract2=qr/^\*\*\* ([^\s]+).*\n--- ([^\s]+)\s*(.*)\n/;
37 my $context_contents=qr/^([!+\-\\ \n]|-{3} .* -{4}|\*{3} .* \*{4}|\*{15})/;
39 my $ls_extract_id=qr/^[^\s]+\s+[^\s]+\s+([^\s]+)\s+([^\s]+)/;
40 my $basename=qr|^(.*/)*([^/]+)$|;
42 sub patchfs_canonicalize_path ($) {
43   my ($fname) = @_;
44   $fname =~ s,/+,/,g;
45   $fname =~ s,(^|/)(?:\.?\./)+,$1,;
46   return $fname;
49 # output unix date in a mc-readable format
50 sub timef
52     my @time=localtime($_[0]);
53     return sprintf '%02d-%02d-%02d %02d:%02d', $time[4]+1, $time[3],
54                    $time[5]+1900, $time[2], $time[1];
57 # parse given string as a date and return unix time
58 sub datetime
60     # in case of problems fall back to 0 in unix time
61     # note: str2time interprets some wrong values (eg. " ") as 'today'
62     if ($parsedates && defined (my $t=str2time($_[0]))) {
63         return timef($t);
64     }
65     return timef(time);
68 # print message on stderr and exit
69 sub error
71     print STDERR $_[0], "\n";
72     exit 1;
75 # (compressed) input
76 sub myin
78     my ($qfname)=(quotemeta $_[0]);
80     $_=`$fileutil $qfname`;
81     if (/^'*lzma/) {
82         return "$lzma -dc $qfname";
83     } elsif (/^'*xz/) {
84         return "$xz -dc $qfname";
85     } elsif (/^'*bzip/) {
86         return "$bzip -dc $qfname";
87     } elsif (/^'*gzip/) {
88         return "$gzip -dc $qfname";
89     } else {
90         return "cat $qfname";
91     }
94 # (compressed) output
95 sub myout
97     my ($qfname,$append)=(quotemeta $_[0],$_[1]);
98     my ($sep) = $append ? '>>' : '>';
100     $_=`$fileutil $qfname`;
101     if (/^'*lzma/) {
102         return "$lzma -c $sep $qfname";
103     } elsif (/^'*xz/) {
104         return "$xz -c $sep $qfname";
105     } elsif (/^'*bzip/) {
106         return "$bzip -c $sep $qfname";
107     } elsif (/^'*gzip/) {
108         return "$gzip -c $sep $qfname";
109     } else {
110         return "cat $sep $qfname";
111     }
114 # select diff filename conforming with rules found in diff.info
115 sub diff_filename
117     my ($fsrc,$fdst)= @_;
118     # TODO: can remove these two calls later
119     $fsrc = patchfs_canonicalize_path ($fsrc);
120     $fdst = patchfs_canonicalize_path ($fdst);
121     if (!$fdst && !$fsrc) {
122         error 'Index: not yet implemented';
123     } elsif (!$fsrc || $fsrc eq '/dev/null') {
124         return ($fdst,'PATCH-CREATE/');
125     } elsif (!$fdst || $fdst eq '/dev/null') {
126         return ($fsrc,'PATCH-REMOVE/');
127     } elsif (($fdst eq '/dev/null') && ($fsrc eq '/dev/null')) {
128         error 'Malformed diff, missing a sane filename';
129     } else {
130         # fewest path name components
131         if ($fdst=~s|/|/|g < $fsrc=~s|/|/|g) {
132             return ($fdst,'');
133         } elsif ($fdst=~s|/|/|g > $fsrc=~s|/|/|g) {
134             return ($fsrc,'');
135         } else {
136             # shorter base name
137             if (($fdst=~/$basename/o,length $2) < ($fsrc=~/$basename/o,length $2)) {
138                 return ($fdst,'');
139             } elsif (($fdst=~/$basename/o,length $2) > ($fsrc=~/$basename/o,length $2)) {
140                 return ($fsrc,'');
141             } else {
142                 # shortest names
143                 if (length $fdst < length $fsrc) {
144                     return ($fdst,'');
145                 } else {
146                     return ($fsrc,'');
147                 }
148             }
149         }
150     }
153 # IN: diff "archive" name
154 # IN: file handle for output; STDIN for list, tempfile else
155 # IN: filename to watch (for: copyout, rm), '' for: list
156 # IN: remove the file?
157 #     true  - ... and print out the rest
158 #     false - ie. copyout mode, print just the file
159 sub parse($$$$)
161     my $archive=quotemeta shift;
162     my $fh=shift;
163     my $file=shift;
164     my $rmmod=shift;
165     my ($state,$fsize,$time);
166     my ($f,$fsrc,$fdst,$prefix);
167     my ($unified,$context);
168     my ($skipread, $filetoprint, $filefound);
169     my ($h_add,$h_del,$h_ctx);  # hunk line counts
170     my ($h_r1,$h_r2);           # hunk ranges
171     my @outsrc;         # if desired ...
172     my @outdst;
173     my $line;
174     my %fmap_size=();
175     my %fmap_time=();
177     import Date::Parse if ($parsedates && $file eq '');
179     $line=1;
180     $state=0; $fsize=0; $f='';
181     $filefound=0;
182     while ($skipread || ($line++,$_=<I>)) {
183         $skipread=0;
184         if($state == 0) {       # expecting comments
185             $unified=$context=0;
186             $unified=1 if (/^--- /);
187             $context=1 if (/^\*\*\* /);
188             if (!$unified && !$context) {
189                 $filefound=0 if($file ne '' && $filetoprint);
190                 # shortcut for rmmod xor filefound
191                 # - in rmmod we print if not found
192                 # - in copyout (!rmmod) we print if found
193                 print $fh $_ if($rmmod != $filefound);
194                 next;
195             }
197             if($file eq '' && $filetoprint) {
198                 $fmap_size{"$prefix$f"}+=$fsize;
199                 $fmap_time{"$prefix$f"}=$time;
200             }
202             # start of new file
203             $_ .=<I>;   # steal next line, both formats
204             $line++;
205             if($unified) {
206                 if(/$unified_header/o) {
207                     ($fsrc,$fdst,$time) = /$unified_extract/o;
208                 } elsif(/$unified_header2/o) {
209                     ($fsrc,$fdst,$time) = /$unified_extract2/o;
210                 } else {
211                     error "Can't parse unified diff header";
212                 }
213             } elsif($context) {
214                 if(/$context_header/o) {
215                     ($fsrc,$fdst,$time) = /$context_extract/o;
216                 } elsif(/$context_header2/o) {
217                     ($fsrc,$fdst,$time) = /$context_extract2/o;
218                 } else {
219                     error "Can't parse context diff header";
220                 }
221             } else {
222                 error "Unrecognized diff header";
223             }
224             $fsrc=patchfs_canonicalize_path($fsrc);
225             $fdst=patchfs_canonicalize_path($fdst);
226             if(wantarray) {
227                 push @outsrc,$fsrc;
228                 push @outdst,$fdst;
229             }
230             ($f,$prefix)=diff_filename($fsrc,$fdst);
231             $filefound=($f eq $file);
233             $f="$f.diff";
234             $filetoprint=1;
235             $fsize=length;
236             print $fh $_ if($rmmod != $filefound);
238             $state=1;
239         } elsif($state == 1) { # expecting diff hunk headers, end of file or comments
240             if($unified) {
241                 my ($a,$b,$c,$d);
242                 ($a,$b,$h_r1,$c,$d,$h_r2)=/$unified_hunk/o;
243                 if(!defined($a) || !defined($c)) {
244                     # hunk header does not come, a comment inside
245                     # or maybe a new file, state 0 will decide
246                     $skipread=1;
247                     $state=0;
248                     next;
249                 }
250                 $fsize+=length;
251                 print $fh $_ if($rmmod != $filefound);
252                 $h_r1=1 if(!defined($b));
253                 $h_r2=1 if(!defined($d));
254                 $h_add=$h_del=$h_ctx=0;
255                 $state=2;
256             } elsif($context) {
257                 if(!/$context_contents/o) {
258                     $skipread=1;
259                     $state=0;
260                     next;
261                 }
262                 print $fh $_ if($rmmod != $filefound);
263                 $fsize+=length;
264             }
265         } elsif($state == 2) { # expecting hunk contents
266             if($h_del + $h_ctx == $h_r1 && $h_add + $h_ctx == $h_r2) {
267                 # hooray, end of hunk
268                 # we optimistically ended with a hunk before but
269                 # the line has been read already
270                 $skipread=1;
271                 $state=1;
272                 next;
273             }
274             print $fh $_ if($rmmod != $filefound);
275             $fsize+=length;
276             my ($first)= /^(.)/;
277             if(ord($first) == ord('+')) { $h_add++; }
278             elsif(ord($first) == ord('-')) { $h_del++; }
279             elsif(ord($first) == ord(' ')) { $h_ctx++; }
280             elsif(ord($first) == ord('\\')) { 0; }
281             elsif(ord($first) == ord('@')) { error "Malformed hunk, header came too early"; }
282             else { error "Unrecognized character in hunk"; }
283         }
284     }
285     if($file eq '' && $filetoprint) {
286         $fmap_size{"$prefix$f"}+=$fsize;
287         $fmap_time{"$prefix$f"}=$time;
288     }
290     # use uid and gid from file
291     my ($uid,$gid)=(`ls -l $archive`=~/$ls_extract_id/o);
293     # flush all file names with cumulative file size
294     while(my ($fn, $fs) = each %fmap_size) {
295         printf $fh "-rw-r--r-- 1 %s %s %d %s %s\n", $uid, $gid, $fs, datetime($fmap_time{$fn}), $fn;
296     }
298     close($fh) if($file ne '');
299     return \(@outsrc, @outdst) if wantarray;
302 # list files affected by patch
303 sub list($) {
304         parse($_[0], *STDOUT, '', 0);
305         close(I);
308 # extract diff from patch
309 # IN: diff file to find
310 # IN: output file name
311 sub copyout($$) {
312     my ($file,$out)=@_;
314     $file=~s/^(PATCH-(CREATE|REMOVE)\/)?(.*)\.diff$/$3/;
315     $file = patchfs_canonicalize_path ($file);
317     open(FH, ">$out") or error("Cannot open output file");
318     parse('', *FH, $file, 0);
321 # remove diff(s) from patch
322 # IN: archive
323 # IN: file to delete
324 sub rm($$) {
325     my $archive=shift;
326     my ($tmp,$tmpname)=tempfile();
328     @_=map {scalar(s/^(PATCH-(CREATE|REMOVE)\/)?(.*)\.diff$/$3/,$_)} @_;
330     # just the first file for now
331     parse($archive, $tmp, $_[0], 1);
332     close I;
334     # replace archive
335     system("cat \Q$tmpname\E | " . myout($archive,0))==0
336       or error "Can't write to archive";
337     system("rm -f -- \Q$tmpname\E");
340 # append diff to archive
341 # IN: diff archive name
342 # IN: newly created file name in archive
343 # IN: the real source file
344 sub copyin($$$) {
345     # TODO: seems to be tricky. what to do?
346     # copyin of file which is already there may:
347     #  * delete the original and copy only the new
348     #  * just append the new hunks to the same file
349     #    problems: may not be a valid diff, unmerged hunks
350     #  * try to merge the two together
351     #    ... but we do not want write patchutils again, right?
352     error "Copying files into diff not supported";
353     return;
355     my ($archive,$name,$src)=@_;
357     # in case we are appending another diff, we have
358     # to delete/merge all the files
359     open(DEVNULL, ">/dev/null");
360     open I, myin($src).'|';
361     my ($srclist,$dstlist)=parse($archive, *DEVNULL, '', 0);
362     close(I);
363     close(DEVNULL);
364     foreach(@$srclist) {
365         print("SRC: del $_\n");
366     }
367     foreach(@$dstlist) {
368         print("DST: del $_\n");
369     }
370     return;
372     # remove overwritten file
373     open I, myin($archive).'|';
374     rm ($archive, $name);
375     close I;
377     my $cmd1=myin("$src.diff");
378     my $cmd2=myout($archive,1);
379     system("$cmd1 | $cmd2")==0
380       or error "Can't write to archive";
383 my $fin = $ARGV[1];
385 # resolve symlink
386 while (-l $fin) {
387     $fin = readlink $fin;
390 if ($ARGV[0] eq 'list') {
391     open I, myin($fin).'|';
392     list ($fin);
393     exit 0;
394 } elsif ($ARGV[0] eq 'copyout') {
395     open I, myin($fin)."|";
396     copyout ($ARGV[2], $ARGV[3]);
397     exit 0;
398 } elsif ($ARGV[0] eq 'rm') {
399     open I, myin($fin)."|";
400     rm ($fin, $ARGV[2]);
401     exit 0;
402 } elsif ($ARGV[0] eq 'rmdir') {
403     exit 0;
404 } elsif ($ARGV[0] eq 'mkdir') {
405     exit 0;
406 } elsif ($ARGV[0] eq 'copyin') {
407     copyin ($fin, $ARGV[2], $ARGV[3]);
408     exit 0;
410 exit 1;