OLD | NEW |
| (Empty) |
1 # Copyright © 2008 Raphaël Hertzog <hertzog@debian.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 2 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 <https://www.gnu.org/licenses/>. | |
15 | |
16 package Dpkg::Source::Patch; | |
17 | |
18 use strict; | |
19 use warnings; | |
20 | |
21 our $VERSION = '0.01'; | |
22 | |
23 use Dpkg; | |
24 use Dpkg::Gettext; | |
25 use Dpkg::IPC; | |
26 use Dpkg::ErrorHandling; | |
27 use Dpkg::Source::Functions qw(fs_time); | |
28 | |
29 use POSIX qw(:errno_h :sys_wait_h); | |
30 use File::Find; | |
31 use File::Basename; | |
32 use File::Spec; | |
33 use File::Path; | |
34 use File::Compare; | |
35 use Fcntl ':mode'; | |
36 #XXX: Needed for sub-second timestamps, require recent perl | |
37 #use Time::HiRes qw(stat); | |
38 | |
39 use parent qw(Dpkg::Compression::FileHandle); | |
40 | |
41 sub create { | |
42 my ($self, %opts) = @_; | |
43 $self->ensure_open('w'); # Creates the file | |
44 *$self->{errors} = 0; | |
45 *$self->{empty} = 1; | |
46 if ($opts{old} and $opts{new}) { | |
47 $opts{old} = '/dev/null' unless -e $opts{old}; | |
48 $opts{new} = '/dev/null' unless -e $opts{new}; | |
49 if (-d $opts{old} and -d $opts{new}) { | |
50 $self->add_diff_directory($opts{old}, $opts{new}, %opts); | |
51 } elsif (-f $opts{old} and -f $opts{new}) { | |
52 $self->add_diff_file($opts{old}, $opts{new}, %opts); | |
53 } else { | |
54 $self->_fail_not_same_type($opts{old}, $opts{new}); | |
55 } | |
56 $self->finish() unless $opts{nofinish}; | |
57 } | |
58 } | |
59 | |
60 sub set_header { | |
61 my ($self, $header) = @_; | |
62 *$self->{header} = $header; | |
63 } | |
64 | |
65 sub add_diff_file { | |
66 my ($self, $old, $new, %opts) = @_; | |
67 $opts{include_timestamp} = 0 unless exists $opts{include_timestamp}; | |
68 my $handle_binary = $opts{handle_binary_func} || sub { | |
69 my ($self, $old, $new) = @_; | |
70 $self->_fail_with_msg($new, _g('binary file contents changed')); | |
71 }; | |
72 # Optimization to avoid forking diff if unnecessary | |
73 return 1 if compare($old, $new, 4096) == 0; | |
74 # Default diff options | |
75 my @options; | |
76 if ($opts{options}) { | |
77 push @options, @{$opts{options}}; | |
78 } else { | |
79 push @options, '-p'; | |
80 } | |
81 # Add labels | |
82 if ($opts{label_old} and $opts{label_new}) { | |
83 if ($opts{include_timestamp}) { | |
84 my $ts = (stat($old))[9]; | |
85 my $t = POSIX::strftime('%Y-%m-%d %H:%M:%S', gmtime($ts)); | |
86 $opts{label_old} .= sprintf("\t%s.%09d +0000", $t, | |
87 ($ts - int($ts)) * 1_000_000_000); | |
88 $ts = (stat($new))[9]; | |
89 $t = POSIX::strftime('%Y-%m-%d %H:%M:%S', gmtime($ts)); | |
90 $opts{label_new} .= sprintf("\t%s.%09d +0000", $t, | |
91 ($ts - int($ts)) * 1_000_000_000); | |
92 } else { | |
93 # Space in filenames need special treatment | |
94 $opts{label_old} .= "\t" if $opts{label_old} =~ / /; | |
95 $opts{label_new} .= "\t" if $opts{label_new} =~ / /; | |
96 } | |
97 push @options, '-L', $opts{label_old}, | |
98 '-L', $opts{label_new}; | |
99 } | |
100 # Generate diff | |
101 my $diffgen; | |
102 my $diff_pid = spawn( | |
103 exec => [ 'diff', '-u', @options, '--', $old, $new ], | |
104 env => { LC_ALL => 'C', LANG => 'C', TZ => 'UTC0' }, | |
105 to_pipe => \$diffgen, | |
106 ); | |
107 # Check diff and write it in patch file | |
108 my $difflinefound = 0; | |
109 my $binary = 0; | |
110 while (<$diffgen>) { | |
111 if (m/^(?:binary|[^-+\@ ].*\bdiffer\b)/i) { | |
112 $binary = 1; | |
113 &$handle_binary($self, $old, $new); | |
114 last; | |
115 } elsif (m/^[-+\@ ]/) { | |
116 $difflinefound++; | |
117 } elsif (m/^\\ /) { | |
118 warning(_g('file %s has no final newline (either ' . | |
119 'original or modified version)'), $new); | |
120 } else { | |
121 chomp; | |
122 error(_g("unknown line from diff -u on %s: `%s'"), $new, $_); | |
123 } | |
124 if (*$self->{empty} and defined(*$self->{header})) { | |
125 $self->print(*$self->{header}) or syserr(_g('failed to write')); | |
126 *$self->{empty} = 0; | |
127 } | |
128 print { $self } $_ or syserr(_g('failed to write')); | |
129 } | |
130 close($diffgen) or syserr('close on diff pipe'); | |
131 wait_child($diff_pid, nocheck => 1, | |
132 cmdline => "diff -u @options -- $old $new"); | |
133 # Verify diff process ended successfully | |
134 # Exit code of diff: 0 => no difference, 1 => diff ok, 2 => error | |
135 # Ignore error if binary content detected | |
136 my $exit = WEXITSTATUS($?); | |
137 unless (WIFEXITED($?) && ($exit == 0 || $exit == 1 || $binary)) { | |
138 subprocerr(_g('diff on %s'), $new); | |
139 } | |
140 return ($exit == 0 || $exit == 1); | |
141 } | |
142 | |
143 sub add_diff_directory { | |
144 my ($self, $old, $new, %opts) = @_; | |
145 # TODO: make this function more configurable | |
146 # - offer to disable some checks | |
147 my $basedir = $opts{basedirname} || basename($new); | |
148 my $inc_removal = $opts{include_removal} || 0; | |
149 my $diff_ignore; | |
150 if ($opts{diff_ignore_func}) { | |
151 $diff_ignore = $opts{diff_ignore_func}; | |
152 } elsif ($opts{diff_ignore_regex}) { | |
153 $diff_ignore = sub { return $_[0] =~ /$opts{diff_ignore_regex}/o }; | |
154 } else { | |
155 $diff_ignore = sub { return 0 }; | |
156 } | |
157 | |
158 my @diff_files; | |
159 my %files_in_new; | |
160 my $scan_new = sub { | |
161 my $fn = (length > length($new)) ? substr($_, length($new) + 1) : '.'; | |
162 return if &$diff_ignore($fn); | |
163 $files_in_new{$fn} = 1; | |
164 lstat("$new/$fn") or syserr(_g('cannot stat file %s'), "$new/$fn"); | |
165 my $mode = S_IMODE((lstat(_))[2]); | |
166 my $size = (lstat(_))[7]; | |
167 if (-l _) { | |
168 unless (-l "$old/$fn") { | |
169 $self->_fail_not_same_type("$old/$fn", "$new/$fn"); | |
170 return; | |
171 } | |
172 my $n = readlink("$new/$fn"); | |
173 unless (defined $n) { | |
174 syserr(_g('cannot read link %s'), "$new/$fn"); | |
175 } | |
176 my $n2 = readlink("$old/$fn"); | |
177 unless (defined $n2) { | |
178 syserr(_g('cannot read link %s'), "$old/$fn"); | |
179 } | |
180 unless ($n eq $n2) { | |
181 $self->_fail_not_same_type("$old/$fn", "$new/$fn"); | |
182 } | |
183 } elsif (-f _) { | |
184 my $old_file = "$old/$fn"; | |
185 if (not lstat("$old/$fn")) { | |
186 if ($! != ENOENT) { | |
187 syserr(_g('cannot stat file %s'), "$old/$fn"); | |
188 } | |
189 $old_file = '/dev/null'; | |
190 } elsif (not -f _) { | |
191 $self->_fail_not_same_type("$old/$fn", "$new/$fn"); | |
192 return; | |
193 } | |
194 | |
195 my $label_old = "$basedir.orig/$fn"; | |
196 if ($opts{use_dev_null}) { | |
197 $label_old = $old_file if $old_file eq '/dev/null'; | |
198 } | |
199 push @diff_files, [$fn, $mode, $size, $old_file, "$new/$fn", | |
200 $label_old, "$basedir/$fn"]; | |
201 } elsif (-p _) { | |
202 unless (-p "$old/$fn") { | |
203 $self->_fail_not_same_type("$old/$fn", "$new/$fn"); | |
204 } | |
205 } elsif (-b _ || -c _ || -S _) { | |
206 $self->_fail_with_msg("$new/$fn", | |
207 _g('device or socket is not allowed')); | |
208 } elsif (-d _) { | |
209 if (not lstat("$old/$fn")) { | |
210 if ($! != ENOENT) { | |
211 syserr(_g('cannot stat file %s'), "$old/$fn"); | |
212 } | |
213 } elsif (not -d _) { | |
214 $self->_fail_not_same_type("$old/$fn", "$new/$fn"); | |
215 } | |
216 } else { | |
217 $self->_fail_with_msg("$new/$fn", _g('unknown file type')); | |
218 } | |
219 }; | |
220 my $scan_old = sub { | |
221 my $fn = (length > length($old)) ? substr($_, length($old) + 1) : '.'; | |
222 return if &$diff_ignore($fn); | |
223 return if $files_in_new{$fn}; | |
224 lstat("$old/$fn") or syserr(_g('cannot stat file %s'), "$old/$fn"); | |
225 if (-f _) { | |
226 if ($inc_removal) { | |
227 push @diff_files, [$fn, 0, 0, "$old/$fn", '/dev/null', | |
228 "$basedir.orig/$fn", '/dev/null']; | |
229 } else { | |
230 warning(_g('ignoring deletion of file %s'), $fn); | |
231 } | |
232 } elsif (-d _) { | |
233 warning(_g('ignoring deletion of directory %s'), $fn); | |
234 } elsif (-l _) { | |
235 warning(_g('ignoring deletion of symlink %s'), $fn); | |
236 } else { | |
237 $self->_fail_not_same_type("$old/$fn", "$new/$fn"); | |
238 } | |
239 }; | |
240 | |
241 find({ wanted => $scan_new, no_chdir => 1 }, $new); | |
242 find({ wanted => $scan_old, no_chdir => 1 }, $old); | |
243 | |
244 if ($opts{order_from} and -e $opts{order_from}) { | |
245 my $order_from = Dpkg::Source::Patch->new( | |
246 filename => $opts{order_from}); | |
247 my $analysis = $order_from->analyze($basedir, verbose => 0); | |
248 my %patchorder; | |
249 my $i = 0; | |
250 foreach my $fn (@{$analysis->{patchorder}}) { | |
251 $fn =~ s{^[^/]+/}{}; | |
252 $patchorder{$fn} = $i++; | |
253 } | |
254 # 'quilt refresh' sorts files as follows: | |
255 # - Any files in the existing patch come first, in the order in | |
256 # which they appear in the existing patch. | |
257 # - New files follow, sorted lexicographically. | |
258 # This seems a reasonable policy to follow, and avoids autopatches | |
259 # being shuffled when they are regenerated. | |
260 foreach my $diff_file (sort { $a->[0] cmp $b->[0] } @diff_files) { | |
261 my $fn = $diff_file->[0]; | |
262 $patchorder{$fn} = $i++ unless exists $patchorder{$fn}; | |
263 } | |
264 @diff_files = sort { $patchorder{$a->[0]} <=> $patchorder{$b->[0]} } | |
265 @diff_files; | |
266 } else { | |
267 @diff_files = sort { $a->[0] cmp $b->[0] } @diff_files; | |
268 } | |
269 | |
270 foreach my $diff_file (@diff_files) { | |
271 my ($fn, $mode, $size, | |
272 $old_file, $new_file, $label_old, $label_new) = @$diff_file; | |
273 my $success = $self->add_diff_file($old_file, $new_file, | |
274 label_old => $label_old, | |
275 label_new => $label_new, %opts); | |
276 if ($success and | |
277 $old_file eq '/dev/null' and $new_file ne '/dev/null') { | |
278 if (not $size) { | |
279 warning(_g("newly created empty file '%s' will not " . | |
280 'be represented in diff'), $fn); | |
281 } else { | |
282 if ($mode & (S_IXUSR | S_IXGRP | S_IXOTH)) { | |
283 warning(_g("executable mode %04o of '%s' will " . | |
284 'not be represented in diff'), $mode, $fn) | |
285 unless $fn eq 'debian/rules'; | |
286 } | |
287 if ($mode & (S_ISUID | S_ISGID | S_ISVTX)) { | |
288 warning(_g("special mode %04o of '%s' will not " . | |
289 'be represented in diff'), $mode, $fn); | |
290 } | |
291 } | |
292 } | |
293 } | |
294 } | |
295 | |
296 sub finish { | |
297 my ($self) = @_; | |
298 close($self) or syserr(_g('cannot close %s'), $self->get_filename()); | |
299 return not *$self->{errors}; | |
300 } | |
301 | |
302 sub register_error { | |
303 my ($self) = @_; | |
304 *$self->{errors}++; | |
305 } | |
306 sub _fail_with_msg { | |
307 my ($self, $file, $msg) = @_; | |
308 errormsg(_g('cannot represent change to %s: %s'), $file, $msg); | |
309 $self->register_error(); | |
310 } | |
311 sub _fail_not_same_type { | |
312 my ($self, $old, $new) = @_; | |
313 my $old_type = get_type($old); | |
314 my $new_type = get_type($new); | |
315 errormsg(_g('cannot represent change to %s:'), $new); | |
316 errormsg(_g(' new version is %s'), $new_type); | |
317 errormsg(_g(' old version is %s'), $old_type); | |
318 $self->register_error(); | |
319 } | |
320 | |
321 sub _getline { | |
322 my $handle = shift; | |
323 | |
324 my $line = <$handle>; | |
325 if (defined $line) { | |
326 # Strip end-of-line chars | |
327 chomp($line); | |
328 $line =~ s/\r$//; | |
329 } | |
330 return $line; | |
331 } | |
332 | |
333 sub _intuit_file_patched { | |
334 my ($old, $new) = @_; | |
335 | |
336 return $new unless defined $old; | |
337 return $old unless defined $new; | |
338 return $new if -e $new and not -e $old; | |
339 return $old if -e $old and not -e $new; | |
340 | |
341 # We don't consider the case where both files are non-existent and | |
342 # where patch picks the one with the fewest directories to create | |
343 # since dpkg-source will pre-create the required directories | |
344 | |
345 # Precalculate metrics used by patch | |
346 my ($tmp_o, $tmp_n) = ($old, $new); | |
347 my ($len_o, $len_n) = (length($old), length($new)); | |
348 $tmp_o =~ s{[/\\]+}{/}g; | |
349 $tmp_n =~ s{[/\\]+}{/}g; | |
350 my $nb_comp_o = ($tmp_o =~ tr{/}{/}); | |
351 my $nb_comp_n = ($tmp_n =~ tr{/}{/}); | |
352 $tmp_o =~ s{^.*/}{}; | |
353 $tmp_n =~ s{^.*/}{}; | |
354 my ($blen_o, $blen_n) = (length($tmp_o), length($tmp_n)); | |
355 | |
356 # Decide like patch would | |
357 if ($nb_comp_o != $nb_comp_n) { | |
358 return ($nb_comp_o < $nb_comp_n) ? $old : $new; | |
359 } elsif ($blen_o != $blen_n) { | |
360 return ($blen_o < $blen_n) ? $old : $new; | |
361 } elsif ($len_o != $len_n) { | |
362 return ($len_o < $len_n) ? $old : $new; | |
363 } | |
364 return $old; | |
365 } | |
366 | |
367 # Fetch the header filename ignoring the optional timestamp | |
368 sub _fetch_filename { | |
369 my ($diff, $header) = @_; | |
370 | |
371 # Strip any leading spaces. | |
372 $header =~ s/^\s+//; | |
373 | |
374 # Is it a C-style string? | |
375 if ($header =~ m/^"/) { | |
376 error(_g('diff %s patches file with C-style encoded filename'), $diff); | |
377 } else { | |
378 # Tab is the official separator, it's always used when | |
379 # filename contain spaces. Try it first, otherwise strip on space | |
380 # if there's no tab | |
381 $header =~ s/\s.*// unless $header =~ s/\t.*//; | |
382 } | |
383 | |
384 return $header; | |
385 } | |
386 | |
387 # check diff for sanity, find directories to create as a side effect | |
388 sub analyze { | |
389 my ($self, $destdir, %opts) = @_; | |
390 | |
391 $opts{verbose} //= 1; | |
392 my $diff = $self->get_filename(); | |
393 my %filepatched; | |
394 my %dirtocreate; | |
395 my @patchorder; | |
396 my $patch_header = ''; | |
397 my $diff_count = 0; | |
398 | |
399 $_ = _getline($self); | |
400 | |
401 HUNK: | |
402 while (defined($_) or not eof($self)) { | |
403 my (%path, %fn); | |
404 # skip comments leading up to patch (if any) | |
405 while (1) { | |
406 if (/^(?:--- |\+\+\+ |@@ -)/) { | |
407 last; | |
408 } else { | |
409 $patch_header .= "$_\n"; | |
410 } | |
411 last HUNK if not defined($_ = _getline($self)); | |
412 } | |
413 $diff_count++; | |
414 # read file header (---/+++ pair) | |
415 unless(s/^--- //) { | |
416 error(_g("expected ^--- in line %d of diff `%s'"), $., $diff); | |
417 } | |
418 $path{old} = $_ = _fetch_filename($diff, $_); | |
419 $fn{old} = $_ if $_ ne '/dev/null' and s{^[^/]*/+}{$destdir/}; | |
420 if (/\.dpkg-orig$/) { | |
421 error(_g("diff `%s' patches file with name ending .dpkg-orig"), $dif
f); | |
422 } | |
423 | |
424 unless (defined($_ = _getline($self))) { | |
425 error(_g("diff `%s' finishes in middle of ---/+++ (line %d)"), $diff
, $.); | |
426 } | |
427 unless (s/^\+\+\+ //) { | |
428 error(_g("line after --- isn't as expected in diff `%s' (line %d)"),
$diff, $.); | |
429 } | |
430 $path{new} = $_ = _fetch_filename($diff, $_); | |
431 $fn{new} = $_ if $_ ne '/dev/null' and s{^[^/]*/+}{$destdir/}; | |
432 | |
433 unless (defined $fn{old} or defined $fn{new}) { | |
434 error(_g("none of the filenames in ---/+++ are valid in diff '%s' (l
ine %d)"), | |
435 $diff, $.); | |
436 } | |
437 | |
438 # Safety checks on both filenames that patch could use | |
439 foreach my $key ('old', 'new') { | |
440 next unless defined $fn{$key}; | |
441 if ($path{$key} =~ m{/\.\./}) { | |
442 error(_g('%s contains an insecure path: %s'), $diff, $path{$key}
); | |
443 } | |
444 my $path = $fn{$key}; | |
445 while (1) { | |
446 if (-l $path) { | |
447 error(_g('diff %s modifies file %s through a symlink: %s'), | |
448 $diff, $fn{$key}, $path); | |
449 } | |
450 last unless $path =~ s{/+[^/]*$}{}; | |
451 last if length($path) <= length($destdir); # $destdir is assumed
safe | |
452 } | |
453 } | |
454 | |
455 if ($path{old} eq '/dev/null' and $path{new} eq '/dev/null') { | |
456 error(_g("original and modified files are /dev/null in diff `%s' (li
ne %d)"), | |
457 $diff, $.); | |
458 } elsif ($path{new} eq '/dev/null') { | |
459 error(_g("file removal without proper filename in diff `%s' (line %d
)"), | |
460 $diff, $. - 1) unless defined $fn{old}; | |
461 if ($opts{verbose}) { | |
462 warning(_g('diff %s removes a non-existing file %s (line %d)'), | |
463 $diff, $fn{old}, $.) unless -e $fn{old}; | |
464 } | |
465 } | |
466 my $fn = _intuit_file_patched($fn{old}, $fn{new}); | |
467 | |
468 my $dirname = $fn; | |
469 if ($dirname =~ s{/[^/]+$}{} and not -d $dirname) { | |
470 $dirtocreate{$dirname} = 1; | |
471 } | |
472 | |
473 if (-e $fn and not -f _) { | |
474 error(_g("diff `%s' patches something which is not a plain file"), $
diff); | |
475 } | |
476 | |
477 if ($filepatched{$fn}) { | |
478 warning(_g("diff `%s' patches file %s twice"), $diff, $fn) | |
479 if $opts{verbose}; | |
480 } else { | |
481 $filepatched{$fn} = 1; | |
482 push @patchorder, $fn; | |
483 } | |
484 | |
485 # read hunks | |
486 my $hunk = 0; | |
487 while (defined($_ = _getline($self))) { | |
488 # read hunk header (@@) | |
489 next if /^\\ /; | |
490 last unless (/^@@ -\d+(,(\d+))? \+\d+(,(\d+))? @\@( .*)?$/); | |
491 my ($olines, $nlines) = ($1 ? $2 : 1, $3 ? $4 : 1); | |
492 # read hunk | |
493 while ($olines || $nlines) { | |
494 unless (defined($_ = _getline($self))) { | |
495 if (($olines == $nlines) and ($olines < 3)) { | |
496 warning(_g("unexpected end of diff `%s'"), $diff) | |
497 if $opts{verbose}; | |
498 last; | |
499 } else { | |
500 error(_g("unexpected end of diff `%s'"), $diff); | |
501 } | |
502 } | |
503 next if /^\\ /; | |
504 # Check stats | |
505 if (/^ / || /^$/) { --$olines; --$nlines; } | |
506 elsif (/^-/) { --$olines; } | |
507 elsif (/^\+/) { --$nlines; } | |
508 else { | |
509 error(_g("expected [ +-] at start of line %d of diff `%s'"), | |
510 $., $diff); | |
511 } | |
512 } | |
513 $hunk++; | |
514 } | |
515 unless($hunk) { | |
516 error(_g("expected ^\@\@ at line %d of diff `%s'"), $., $diff); | |
517 } | |
518 } | |
519 close($self); | |
520 unless ($diff_count) { | |
521 warning(_g("diff `%s' doesn't contain any patch"), $diff) | |
522 if $opts{verbose}; | |
523 } | |
524 *$self->{analysis}{$destdir}{dirtocreate} = \%dirtocreate; | |
525 *$self->{analysis}{$destdir}{filepatched} = \%filepatched; | |
526 *$self->{analysis}{$destdir}{patchorder} = \@patchorder; | |
527 *$self->{analysis}{$destdir}{patchheader} = $patch_header; | |
528 return *$self->{analysis}{$destdir}; | |
529 } | |
530 | |
531 sub prepare_apply { | |
532 my ($self, $analysis, %opts) = @_; | |
533 if ($opts{create_dirs}) { | |
534 foreach my $dir (keys %{$analysis->{dirtocreate}}) { | |
535 eval { mkpath($dir, 0, 0777); }; | |
536 syserr(_g('cannot create directory %s'), $dir) if $@; | |
537 } | |
538 } | |
539 } | |
540 | |
541 sub apply { | |
542 my ($self, $destdir, %opts) = @_; | |
543 # Set default values to options | |
544 $opts{force_timestamp} = 1 unless exists $opts{force_timestamp}; | |
545 $opts{remove_backup} = 1 unless exists $opts{remove_backup}; | |
546 $opts{create_dirs} = 1 unless exists $opts{create_dirs}; | |
547 $opts{options} ||= [ '-t', '-F', '0', '-N', '-p1', '-u', | |
548 '-V', 'never', '-g0', '-b', '-z', '.dpkg-orig']; | |
549 $opts{add_options} ||= []; | |
550 push @{$opts{options}}, @{$opts{add_options}}; | |
551 # Check the diff and create missing directories | |
552 my $analysis = $self->analyze($destdir, %opts); | |
553 $self->prepare_apply($analysis, %opts); | |
554 # Apply the patch | |
555 $self->ensure_open('r'); | |
556 my ($stdout, $stderr) = ('', ''); | |
557 spawn( | |
558 exec => [ 'patch', @{$opts{options}} ], | |
559 chdir => $destdir, | |
560 env => { LC_ALL => 'C', LANG => 'C' }, | |
561 delete_env => [ 'POSIXLY_CORRECT' ], # ensure expected patch behaviour | |
562 wait_child => 1, | |
563 nocheck => 1, | |
564 from_handle => $self->get_filehandle(), | |
565 to_string => \$stdout, | |
566 error_to_string => \$stderr, | |
567 ); | |
568 if ($?) { | |
569 print { *STDOUT } $stdout; | |
570 print { *STDERR } $stderr; | |
571 subprocerr('LC_ALL=C patch ' . join(' ', @{$opts{options}}) . | |
572 ' < ' . $self->get_filename()); | |
573 } | |
574 $self->close(); | |
575 # Reset the timestamp of all the patched files | |
576 # and remove .dpkg-orig files | |
577 my @files = keys %{$analysis->{filepatched}}; | |
578 my $now = $opts{timestamp}; | |
579 $now ||= fs_time($files[0]) if $opts{force_timestamp} && scalar @files; | |
580 foreach my $fn (@files) { | |
581 if ($opts{force_timestamp}) { | |
582 utime($now, $now, $fn) or $! == ENOENT | |
583 or syserr(_g('cannot change timestamp for %s'), $fn); | |
584 } | |
585 if ($opts{remove_backup}) { | |
586 $fn .= '.dpkg-orig'; | |
587 unlink($fn) or syserr(_g('remove patch backup file %s'), $fn); | |
588 } | |
589 } | |
590 return $analysis; | |
591 } | |
592 | |
593 # Verify if check will work... | |
594 sub check_apply { | |
595 my ($self, $destdir, %opts) = @_; | |
596 # Set default values to options | |
597 $opts{create_dirs} = 1 unless exists $opts{create_dirs}; | |
598 $opts{options} ||= [ '--dry-run', '-s', '-t', '-F', '0', '-N', '-p1', '-u', | |
599 '-V', 'never', '-g0', '-b', '-z', '.dpkg-orig']; | |
600 $opts{add_options} ||= []; | |
601 push @{$opts{options}}, @{$opts{add_options}}; | |
602 # Check the diff and create missing directories | |
603 my $analysis = $self->analyze($destdir, %opts); | |
604 $self->prepare_apply($analysis, %opts); | |
605 # Apply the patch | |
606 $self->ensure_open('r'); | |
607 my $patch_pid = spawn( | |
608 exec => [ 'patch', @{$opts{options}} ], | |
609 chdir => $destdir, | |
610 env => { LC_ALL => 'C', LANG => 'C' }, | |
611 delete_env => [ 'POSIXLY_CORRECT' ], # ensure expected patch behaviour | |
612 from_handle => $self->get_filehandle(), | |
613 to_file => '/dev/null', | |
614 error_to_file => '/dev/null', | |
615 ); | |
616 wait_child($patch_pid, nocheck => 1); | |
617 my $exit = WEXITSTATUS($?); | |
618 subprocerr('patch --dry-run') unless WIFEXITED($?); | |
619 $self->close(); | |
620 return ($exit == 0); | |
621 } | |
622 | |
623 # Helper functions | |
624 sub get_type { | |
625 my $file = shift; | |
626 if (not lstat($file)) { | |
627 return _g('nonexistent') if $! == ENOENT; | |
628 syserr(_g('cannot stat %s'), $file); | |
629 } else { | |
630 -f _ && return _g('plain file'); | |
631 -d _ && return _g('directory'); | |
632 -l _ && return sprintf(_g('symlink to %s'), readlink($file)); | |
633 -b _ && return _g('block device'); | |
634 -c _ && return _g('character device'); | |
635 -p _ && return _g('named pipe'); | |
636 -S _ && return _g('named socket'); | |
637 } | |
638 } | |
639 | |
640 1; | |
OLD | NEW |