patchfs.in 10.0 KB

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