Patch.pm 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. # Copyright 2008 Raphaël Hertzog <hertzog@debian.org>
  2. # This program is free software; you can redistribute it and/or modify
  3. # it under the terms of the GNU General Public License as published by
  4. # the Free Software Foundation; either version 2 of the License, or
  5. # (at your option) any later version.
  6. # This program is distributed in the hope that it will be useful,
  7. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  8. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  9. # GNU General Public License for more details.
  10. # You should have received a copy of the GNU General Public License along
  11. # with this program; if not, write to the Free Software Foundation, Inc.,
  12. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  13. package Dpkg::Source::Patch;
  14. use strict;
  15. use warnings;
  16. use Dpkg;
  17. use Dpkg::Source::CompressedFile;
  18. use Dpkg::Source::Compressor;
  19. use Dpkg::Compression;
  20. use Dpkg::Gettext;
  21. use Dpkg::IPC;
  22. use Dpkg::ErrorHandling qw(error syserr warning subprocerr);
  23. use POSIX;
  24. use File::Find;
  25. use File::Basename;
  26. use File::Spec;
  27. use Fcntl ':mode';
  28. use base 'Dpkg::Source::CompressedFile';
  29. sub create {
  30. my ($self, %opts) = @_;
  31. $self->{'handle'} = $self->open_for_write();
  32. $self->{'errors'} = 0;
  33. if ($opts{'old'} and $opts{'new'}) {
  34. $opts{'old'} = "/dev/null" unless -e $opts{'old'};
  35. $opts{'new'} = "/dev/null" unless -e $opts{'new'};
  36. if (-d $opts{'old'} and -d $opts{'new'}) {
  37. $self->add_diff_directory($opts{'old'}, $opts{'new'}, %opts);
  38. } elsif (-f $opts{'old'} and -f $opts{'new'}) {
  39. $self->add_diff_file($opts{'old'}, $opts{'new'}, %opts);
  40. } else {
  41. $self->_fail_not_same_type($opts{'old'}, $opts{'new'});
  42. }
  43. $self->close() unless $opts{"noclose"};
  44. }
  45. }
  46. sub add_diff_file {
  47. my ($self, $old, $new, %opts) = @_;
  48. # Default diff options
  49. my @options;
  50. if ($opts{"options"}) {
  51. push @options, @{$opts{"options"}};
  52. } else {
  53. push @options, '-p';
  54. }
  55. # Add labels
  56. if ($opts{"label_old"} and $opts{"label_new"}) {
  57. # Space in filenames need special treatment
  58. $opts{"label_old"} .= "\t" if $opts{"label_old"} =~ / /;
  59. $opts{"label_new"} .= "\t" if $opts{"label_new"} =~ / /;
  60. push @options, "-L", $opts{"label_old"},
  61. "-L", $opts{"label_new"};
  62. }
  63. # Generate diff
  64. my $diffgen;
  65. my $diff_pid = fork_and_exec(
  66. 'exec' => [ 'diff', '-u', @options, '--', $old, $new ],
  67. 'env' => { LC_ALL => 'C', LANG => 'C', TZ => 'UTC0' },
  68. 'to_pipe' => \$diffgen
  69. );
  70. # Check diff and write it in patch file
  71. my $difflinefound = 0;
  72. while (<$diffgen>) {
  73. if (m/^binary/i) {
  74. $self->_fail_with_msg($new,_g("binary file contents changed"));
  75. last;
  76. } elsif (m/^[-+\@ ]/) {
  77. $difflinefound++;
  78. } elsif (m/^\\ No newline at end of file$/) {
  79. warning(_g("file %s has no final newline (either " .
  80. "original or modified version)"), $new);
  81. } else {
  82. chomp;
  83. internerr(_g("unknown line from diff -u on %s: `%s'"),
  84. $new, $_);
  85. }
  86. print({ $self->{'handle'} } $_) || syserr(_g("failed to write"));
  87. }
  88. close($diffgen) or syserr("close on diff pipe");
  89. wait_child($diff_pid, nocheck => 1,
  90. cmdline => "diff -u @options -- $old $new");
  91. # Verify diff process ended successfully
  92. # Exit code of diff: 0 => no difference, 1 => diff ok, 2 => error
  93. my $exit = WEXITSTATUS($?);
  94. unless (WIFEXITED($?) && ($exit == 0 || $exit == 1)) {
  95. subprocerr(_g("diff on %s"), $new);
  96. }
  97. return $exit;
  98. }
  99. sub add_diff_directory {
  100. my ($self, $old, $new, %opts) = @_;
  101. # TODO: make this function more configurable
  102. # - offer diff generation for removed files
  103. # - offer to disable some checks
  104. my $basedir = $opts{"basedirname"} || basename($new);
  105. my $diff_ignore;
  106. if ($opts{"diff_ignore_func"}) {
  107. $diff_ignore = $opts{"diff_ignore_func"};
  108. } elsif ($opts{"diff_ignore_regexp"}) {
  109. $diff_ignore = sub { return $_[0] =~ /$opts{"diff_ignore_regexp"}/o };
  110. } else {
  111. $diff_ignore = sub { return 0 };
  112. }
  113. my %files_in_new;
  114. my $scan_new = sub {
  115. my $fn = File::Spec->abs2rel($_, $new);
  116. next if &$diff_ignore($fn);
  117. $files_in_new{$fn} = 1;
  118. lstat("$new/$fn") || syserr(_g("cannot stat file %s"), "$new/$fn");
  119. my $mode = S_IMODE((lstat(_))[2]);
  120. my $size = (lstat(_))[7];
  121. if (-l _) {
  122. unless (-l "$old/$fn") {
  123. $self->_fail_not_same_type("$old/$fn", "$new/$fn");
  124. return;
  125. }
  126. defined(my $n = readlink("$new/$fn")) ||
  127. syserr(_g("cannot read link %s"), "$new/$fn");
  128. defined(my $n2 = readlink("$old/$fn")) ||
  129. syserr(_g("cannot read link %s"), "$old/$fn");
  130. unless ($n eq $n2) {
  131. $self->_fail_not_same_type("$old/$fn", "$new/$fn");
  132. }
  133. } elsif (-f _) {
  134. my $old_file = "$old/$fn";
  135. if (not lstat("$old/$fn")) {
  136. $! == ENOENT ||
  137. syserr(_g("cannot stat file %s"), "$old/$fn");
  138. $old_file = '/dev/null';
  139. if (not $size) {
  140. warning(_g("newly created empty file '%s' will not " .
  141. "be represented in diff"), $fn);
  142. } else {
  143. if ($mode & (S_IXUSR | S_IXGRP | S_IXOTH)) {
  144. warning(_g("executable mode %04o of '%s' will " .
  145. "not be represented in diff"), $mode, $fn)
  146. unless $fn eq 'debian/rules';
  147. }
  148. if ($mode & (S_ISUID | S_ISGID | S_ISVTX)) {
  149. warning(_g("special mode %04o of '%s' will not " .
  150. "be represented in diff"), $mode, $fn);
  151. }
  152. }
  153. } elsif (not -f _) {
  154. $self->_fail_not_same_type("$old/$fn", "$new/$fn");
  155. return;
  156. }
  157. $self->add_diff_file($old_file, "$new/$fn",
  158. label_old => "$basedir.orig/$fn",
  159. label_new => "$basedir/$fn",
  160. %opts);
  161. } elsif (-p _) {
  162. unless (-p "$old/$fn") {
  163. $self->_fail_not_same_type("$old/$fn", "$new/$fn");
  164. }
  165. } elsif (-b _ || -c _ || -S _) {
  166. $self->_fail_with_msg("$new/$fn",
  167. _g("device or socket is not allowed"));
  168. } elsif (-d _) {
  169. if (not lstat("$old/$fn")) {
  170. $! == ENOENT ||
  171. syserr(_g("cannot stat file %s"), "$old/$fn");
  172. } elsif (not -d _) {
  173. $self->_fail_not_same_type("$old/$fn", "$new/$fn");
  174. }
  175. } else {
  176. $self->_fail_with_msg("$new/$fn", _g("unknown file type"));
  177. }
  178. };
  179. my $scan_old = sub {
  180. my $fn = File::Spec->abs2rel($_, $old);
  181. return if &$diff_ignore($fn);
  182. return if $files_in_new{$fn};
  183. lstat("$new/$fn") || syserr(_g("cannot stat file %s"), "$old/$fn");
  184. if (-f _) {
  185. warning(_g("ignoring deletion of file %s"), $fn);
  186. } elsif (-d _) {
  187. warning(_g("ignoring deletion of directory %s"), $fn);
  188. } elsif (-l _) {
  189. warning(_g("ignoring deletion of symlink %s"), $fn);
  190. } else {
  191. $self->_fail_not_same_type("$old/$fn", "$new/$fn");
  192. }
  193. };
  194. find({ wanted => $scan_new, no_chdir => 1 }, $new);
  195. find({ wanted => $scan_old, no_chdir => 1 }, $old);
  196. }
  197. sub close {
  198. my ($self) = @_;
  199. close($self->{'handle'}) ||
  200. syserr(_g("cannot close %s"), $self->get_filename());
  201. delete $self->{'handle'};
  202. $self->cleanup_after_open();
  203. return not $self->{'errors'};
  204. }
  205. sub _fail_with_msg {
  206. my ($self, $file, $msg) = @_;
  207. printf(STDERR _g("%s: cannot represent change to %s: %s")."\n",
  208. $progname, $file, $msg);
  209. $self->{'errors'}++;
  210. }
  211. sub _fail_not_same_type {
  212. my ($self, $old, $new) = @_;
  213. my $old_type = get_type($old);
  214. my $new_type = get_type($new);
  215. printf(STDERR _g("%s: cannot represent change to %s:\n".
  216. "%s: new version is %s\n".
  217. "%s: old version is %s\n"),
  218. $progname, $new, $progname, $old_type, $progname, $new_type);
  219. $self->{'errors'}++;
  220. }
  221. sub apply {
  222. my ($self, $destdir, %opts) = @_;
  223. # TODO: check diff
  224. # TODO: create missing directories
  225. $opts{"options"} ||= [ '-s', '-t', '-F', '0', '-N', '-p1', '-u',
  226. '-V', 'never', '-g0', '-b', '-z', '.dpkg-orig'];
  227. my $diff_handle = $self->open_for_read();
  228. fork_and_exec(
  229. 'exec' => [ 'patch', @{$opts{"options"}} ],
  230. 'chdir' => $destdir,
  231. 'env' => { LC_ALL => 'C', LANG => 'C' },
  232. 'wait_child' => 1,
  233. 'from_handle' => $diff_handle
  234. );
  235. $self->cleanup_after_open();
  236. }
  237. # Helper functions
  238. sub get_type {
  239. my $file = shift;
  240. if (not lstat($file)) {
  241. return _g("nonexistent") if $! == ENOENT;
  242. syserr(_g("cannot stat %s"), $file);
  243. } else {
  244. -f _ && return _g("plain file");
  245. -d _ && return _g("directory");
  246. -l _ && return sprintf(_g("symlink to %"), readlink($file));
  247. -b _ && return _g("block device");
  248. -c _ && return _g("character device");
  249. -p _ && return _g("named pipe");
  250. -S _ && return _g("named socket");
  251. }
  252. }
  253. 1;
  254. # vim: set et sw=4 ts=8