123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 |
- #! @PERL@ -w
- #
- # Written by Adam Byrtek <alpha@debian.org>, 2002
- # Rewritten by David Sterba <dave@jikos.cz>, 2009
- #
- # Extfs to handle patches in context and unified diff format.
- # Known issues: When name of file to patch is modified during editing,
- # hunk is duplicated on copyin. It is unavoidable.
- use bytes;
- use strict;
- use POSIX;
- use File::Temp 'tempfile';
- # standard binaries
- my $lzma = 'lzma';
- my $xz = 'xz';
- my $bzip = 'bzip2';
- my $gzip = 'gzip';
- my $fileutil = 'file -b';
- # date parsing requires Date::Parse from TimeDate module
- my $parsedates = eval 'require Date::Parse';
- # regular expressions
- my $unified_header=qr/^--- .*\n\+\+\+ .*\n$/;
- my $unified_extract=qr/^--- ([^\s]+).*\n\+\+\+ ([^\s]+)\s*([^\t\n]*)/;
- my $unified_contents=qr/^([+\-\\ \n]|@@ .* @@)/;
- my $unified_hunk=qr/@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+)) @@.*\n/;
- my $context_header=qr/^\*\*\* .*\n--- .*\n$/;
- my $context_extract=qr/^\*\*\* ([^\s]+).*\n--- ([^\s]+)\s*([^\t\n]*)/;
- my $context_contents=qr/^([!+\-\\ \n]|-{3} .* -{4}|\*{3} .* \*{4}|\*{15})/;
- my $ls_extract_id=qr/^[^\s]+\s+[^\s]+\s+([^\s]+)\s+([^\s]+)/;
- my $basename=qr|^(.*/)*([^/]+)$|;
- sub patchfs_canonicalize_path ($) {
- my ($fname) = @_;
- $fname =~ s,/+,/,g;
- $fname =~ s,(^|/)(?:\.?\./)+,$1,;
- return $fname;
- }
- # output unix date in a mc-readable format
- sub timef
- {
- my @time=localtime($_[0]);
- return sprintf '%02d-%02d-%02d %02d:%02d', $time[4]+1, $time[3],
- $time[5]+1900, $time[2], $time[1];
- }
- # parse given string as a date and return unix time
- sub datetime
- {
- # in case of problems fall back to 0 in unix time
- # note: str2time interprets some wrong values (eg. " ") as 'today'
- if ($parsedates && defined (my $t=str2time($_[0]))) {
- return timef($t);
- }
- return timef(time);
- }
- # print message on stderr and exit
- sub error
- {
- print STDERR $_[0], "\n";
- exit 1;
- }
- # (compressed) input
- sub myin
- {
- my ($qfname)=(quotemeta $_[0]);
- $_=`$fileutil $qfname`;
- if (/^'*lzma/) {
- return "$lzma -dc $qfname";
- } elsif (/^'*xz/) {
- return "$xz -dc $qfname";
- } elsif (/^'*bzip/) {
- return "$bzip -dc $qfname";
- } elsif (/^'*gzip/) {
- return "$gzip -dc $qfname";
- } else {
- return "cat $qfname";
- }
- }
- # (compressed) output
- sub myout
- {
- my ($qfname,$append)=(quotemeta $_[0],$_[1]);
- my ($sep) = $append ? '>>' : '>';
- $_=`$fileutil $qfname`;
- if (/^'*lzma/) {
- return "$lzma -c $sep $qfname";
- } elsif (/^'*xz/) {
- return "$xz -c $sep $qfname";
- } elsif (/^'*bzip/) {
- return "$bzip -c $sep $qfname";
- } elsif (/^'*gzip/) {
- return "$gzip -c $sep $qfname";
- } else {
- return "cat $sep $qfname";
- }
- }
- # select diff filename conforming with rules found in diff.info
- sub diff_filename
- {
- my ($fsrc,$fdst)= @_;
- # TODO: can remove these two calls later
- $fsrc = patchfs_canonicalize_path ($fsrc);
- $fdst = patchfs_canonicalize_path ($fdst);
- if (!$fdst && !$fsrc) {
- error 'Index: not yet implemented';
- } elsif (!$fsrc || $fsrc eq '/dev/null') {
- return ($fdst,'PATCH-CREATE/');
- } elsif (!$fdst || $fdst eq '/dev/null') {
- return ($fsrc,'PATCH-REMOVE/');
- } elsif (($fdst eq '/dev/null') && ($fsrc eq '/dev/null')) {
- error 'Malformed diff';
- } else {
- # fewest path name components
- if ($fdst=~s|/|/|g < $fsrc=~s|/|/|g) {
- return ($fdst,'');
- } elsif ($fdst=~s|/|/|g > $fsrc=~s|/|/|g) {
- return ($fsrc,'');
- } else {
- # shorter base name
- if (($fdst=~/$basename/o,length $2) < ($fsrc=~/$basename/o,length $2)) {
- return ($fdst,'');
- } elsif (($fdst=~/$basename/o,length $2) > ($fsrc=~/$basename/o,length $2)) {
- return ($fsrc,'');
- } else {
- # shortest names
- if (length $fdst < length $fsrc) {
- return ($fdst,'');
- } else {
- return ($fsrc,'');
- }
- }
- }
- }
- }
- # IN: diff "archive" name
- # IN: file handle for output; STDIN for list, tempfile else
- # IN: filename to watch (for: copyout, rm), '' for: list
- # IN: remove the file?
- # true - ... and print out the rest
- # false - ie. copyout mode, print just the file
- sub parse($$$$)
- {
- my $archive=quotemeta shift;
- my $fh=shift;
- my $file=shift;
- my $rmmod=shift;
- my ($state,$fsize,$time);
- my ($f,$fsrc,$fdst,$prefix);
- my ($unified,$context);
- my ($skipread, $filetoprint, $filefound);
- my ($h_add,$h_del,$h_ctx); # hunk line counts
- my ($h_r1,$h_r2); # hunk ranges
- my @outsrc; # if desired ...
- my @outdst;
- my $line;
- # use uid and gid from file
- my ($uid,$gid)=(`ls -l $archive`=~/$ls_extract_id/o);
- import Date::Parse if ($parsedates && $file eq '');
- $line=1;
- $state=0; $fsize=0; $f='';
- $filefound=0;
- while ($skipread || ($line++,$_=<I>)) {
- $skipread=0;
- if($state == 0) { # expecting comments
- $unified=$context=0;
- $unified=1 if (/^--- /);
- $context=1 if (/^\*\*\* /);
- if (!$unified && !$context) {
- $filefound=0 if($file ne '' && $filetoprint);
- # shortcut for rmmod xor filefound
- # - in rmmod we print if not found
- # - in copyout (!rmmod) we print if found
- print $fh $_ if($rmmod != $filefound);
- next;
- }
- if($file eq '' && $filetoprint) {
- printf $fh "-rw-r--r-- 1 %s %s %d %s %s%s\n", $uid, $gid, $fsize, datetime($time), $prefix, $f;
- }
- # start of new file
- $_ .=<I>; # steel next line, both formats
- $line++;
- if($unified) {
- if(/$unified_header/o) {
- ($fsrc,$fdst,$time) = /$unified_extract/o;
- } else {
- error "Can't parse unified diff header";
- }
- } elsif($context) {
- if(/$context_header/o) {
- ($fsrc,$fdst,$time) = /$context_extract/o;
- } else {
- error "Can't parse context diff header";
- }
- } else {
- error "Unrecognized diff header";
- }
- $fsrc=patchfs_canonicalize_path($fsrc);
- $fdst=patchfs_canonicalize_path($fdst);
- if(wantarray) {
- push @outsrc,$fsrc;
- push @outdst,$fdst;
- }
- ($f,$prefix)=diff_filename($fsrc,$fdst);
- $filefound=($fsrc eq $file || $fdst eq $file);
- $f="$f.diff";
- $filetoprint=1;
- $fsize=length;
- print $fh $_ if($rmmod != $filefound);
- $state=1;
- } elsif($state == 1) { # expecting diff hunk headers, end of file or comments
- if($unified) {
- my ($a,$b,$c,$d);
- ($a,$b,$h_r1,$c,$d,$h_r2)=/$unified_hunk/o;
- if(!defined($a) || !defined($c)) {
- # hunk header does not come, a comment inside
- # or maybe a new file, state 0 will decide
- $skipread=1;
- $state=0;
- next;
- }
- $fsize+=length;
- print $fh $_ if($rmmod != $filefound);
- $h_r1=1 if(!defined($b));
- $h_r2=1 if(!defined($d));
- $h_add=$h_del=$h_ctx=0;
- $state=2;
- } elsif($context) {
- if(!/$context_contents/o) {
- $skipread=1;
- $state=0;
- next;
- }
- print $fh $_ if($rmmod != $filefound);
- $fsize+=length;
- }
- } elsif($state == 2) { # expecting hunk contents
- if($h_del + $h_ctx == $h_r1 && $h_add + $h_ctx == $h_r2) {
- # hooray, end of hunk
- # we optimistically ended with a hunk before but
- # the line has been read already
- $skipread=1;
- $state=1;
- next;
- }
- print $fh $_ if($rmmod != $filefound);
- $fsize+=length;
- my ($first)= /^(.)/;
- if(ord($first) == ord('+')) { $h_add++; }
- elsif(ord($first) == ord('-')) { $h_del++; }
- elsif(ord($first) == ord(' ')) { $h_ctx++; }
- elsif(ord($first) == ord('\\')) { 0; }
- elsif(ord($first) == ord('@')) { error "Malformed hunk, header came too early"; }
- else { error "Unrecognized character in hunk"; }
- }
- }
- if($file eq '' && $filetoprint) {
- printf $fh "-rw-r--r-- 1 %s %s %d %s %s%s\n", $uid, $gid, $fsize, datetime($time), $prefix, $f;
- }
- close($fh) if($file ne '');
- return \(@outsrc, @outdst) if wantarray;
- }
- # list files affected by patch
- sub list($) {
- parse($_[0], *STDOUT, '', 0);
- close(I);
- }
- # extract diff from patch
- # IN: diff file to find
- # IN: output file name
- sub copyout($$) {
- my ($file,$out)=@_;
- $file=~s/^(PATCH-(CREATE|REMOVE)\/)?(.*)\.diff$/$3/;
- $file = patchfs_canonicalize_path ($file);
- open(FH, ">$out") or error("Cannot open output file");
- parse('', *FH, $file, 0);
- }
- # remove diff(s) from patch
- # IN: archive
- # IN: file to delete
- sub rm($$) {
- my $archive=shift;
- my ($tmp,$tmpname)=tempfile();
- @_=map {scalar(s/^(PATCH-(CREATE|REMOVE)\/)?(.*)\.diff$/$3/,$_)} @_;
- # just the first file for now
- parse($archive, $tmp, $_[0], 1);
- close I;
- # replace archive
- system("cat \Q$tmpname\E | " . myout($archive,0))==0
- or error "Can't write to archive";
- system("rm -f -- \Q$tmpname\E");
- }
- # append diff to archive
- # IN: diff archive name
- # IN: newly created file name in archive
- # IN: the real source file
- sub copyin($$$) {
- # TODO: seems to be tricky. what to do?
- # copyin of file which is already there may:
- # * delete the original and copy only the new
- # * just append the new hunks to the same file
- # problems: may not be a valid diff, unmerged hunks
- # * try to merge the two together
- # ... but we do not want write patchutils again, right?
- error "Copying files into diff not supported";
- return;
- my ($archive,$name,$src)=@_;
- # in case we are appending another diff, we have
- # to delete/merge all the files
- open(DEVNULL, ">/dev/null");
- open I, myin($src).'|';
- my ($srclist,$dstlist)=parse($archive, *DEVNULL, '', 0);
- close(I);
- close(DEVNULL);
- foreach(@$srclist) {
- print("SRC: del $_\n");
- }
- foreach(@$dstlist) {
- print("DST: del $_\n");
- }
- return;
- # remove overwritten file
- open I, myin($archive).'|';
- rm ($archive, $name);
- close I;
- my $cmd1=myin("$src.diff");
- my $cmd2=myout($archive,1);
- system("$cmd1 | $cmd2")==0
- or error "Can't write to archive";
- }
- if ($ARGV[0] eq 'list') {
- open I, myin($ARGV[1]).'|';
- list ($ARGV[1]);
- exit 0;
- } elsif ($ARGV[0] eq 'copyout') {
- open I, myin($ARGV[1])."|";
- copyout ($ARGV[2], $ARGV[3]);
- exit 0;
- } elsif ($ARGV[0] eq 'rm') {
- open I, myin($ARGV[1])."|";
- rm ($ARGV[1], $ARGV[2]);
- exit 0;
- } elsif ($ARGV[0] eq 'rmdir') {
- exit 0;
- } elsif ($ARGV[0] eq 'mkdir') {
- exit 0;
- } elsif ($ARGV[0] eq 'copyin') {
- copyin ($ARGV[1], $ARGV[2], $ARGV[3]);
- exit 0;
- }
- exit 1;
|