| 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; | ||||||