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