From 6d37819cec5bce94eff0aed3d44f8498c8c6ed2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 25 Sep 2026 13:15:50 -0300 Subject: [PATCH] test(ipxe-xcat): cover the payload check Damage a copy of a fixture tree one way at a time: a changed byte, a missing file, an extra file or directory, a symlink with another target, and a symlink replaced by a copy of its target. Each must fail the check and name the path. Also hold the committed payload.sha256 and SHA256SUMS to the committed archives. --- t/ipxe_xcat_payload.t | 135 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 t/ipxe_xcat_payload.t diff --git a/t/ipxe_xcat_payload.t b/t/ipxe_xcat_payload.t new file mode 100644 index 0000000..f58ccac --- /dev/null +++ b/t/ipxe_xcat_payload.t @@ -0,0 +1,135 @@ +#!/usr/bin/perl +# Behaviour test for ipxe-xcat/verify-payload.pl, the check between a built ipxe-xcat package and the +# release tree that the package must carry byte for byte. +# +# A manifest is written for a small fixture tree with the checker's own --generate mode. Each case +# damages a copy of the tree in one way and runs the check as a subprocess: the check must fail and +# name the path. A check that compared only names would pass every damage case below. The last block +# holds the committed payload.sha256 and SHA256SUMS to the committed release archives. +use strict; +use warnings; +use Test::More; +use Digest::SHA (); +use File::Basename qw(dirname); +use File::Path qw(make_path); +use File::Temp qw(tempdir); +use FindBin qw($RealBin); +use IPC::Open3 qw(open3); + +my $pkg_dir = "$RealBin/../ipxe-xcat"; +my $checker = "$pkg_dir/verify-payload.pl"; +plan skip_all => 'ipxe-xcat/verify-payload.pl not found' unless -f $checker; + +my $tmp = tempdir(CLEANUP => 1); + +# Runs the checker; returns its exit code and its merged stdout and stderr. +sub run_checker { + my (@args) = @_; + my $pid = open3(my $in, my $out, undef, $^X, $checker, @args); + close($in); + my $text = do { local $/; <$out> } // ''; + waitpid($pid, 0); + return ($? >> 8, $text); +} + +sub write_file { + my ($path, $content) = @_; + make_path(dirname($path)); + open(my $fh, '>:raw', $path) or die "write $path: $!"; + print {$fh} $content; + close($fh) or die "close $path: $!"; +} + +sub make_tree { + my ($root) = @_; + write_file("$root/i386/undionly.kpxe", "undi\x00\x01"); + write_file("$root/x86_64-sb/snponly.efi", "MZ signed snponly"); + write_file("$root/x86_64-sb/shimx64.efi", "MZ signed shim"); + symlink('shimx64.efi', "$root/x86_64-sb/ipxe-shim.efi") or die "symlink: $!"; + symlink('x86_64-sb', "$root/sb") or die "symlink: $!"; + symlink('i386/undionly.kpxe', "$root/undionly.kpxe") or die "symlink: $!"; +} + +sub copy_tree { + my ($name) = @_; + my $copy = "$tmp/$name"; + system('cp', '-a', "$tmp/pristine", $copy) == 0 or die "cp -a to $copy failed"; + return $copy; +} + +make_tree("$tmp/pristine"); +my ($code, $manifest_text) = run_checker('--generate', "$tmp/pristine"); +is($code, 0, '--generate succeeds on a tree of files, directories and symlinks'); +my $manifest = "$tmp/payload.sha256"; +write_file($manifest, $manifest_text); +my @entries = grep { !/^#/ } split(/\n/, $manifest_text); +is(scalar(@entries), 8, 'the manifest has one entry for each directory, file and symlink'); +like($manifest_text, qr/^link\tx86_64-sb\tsb$/m, 'a symlink is recorded with its target, not followed'); + +($code, my $output) = run_checker("$tmp/pristine", $manifest); +is($code, 0, 'the unchanged tree passes') or diag($output); +like($output, qr/payload matches .*: 8 entries/, 'the check reports the number of entries'); +($code, $output) = run_checker("$tmp/pristine/", $manifest); +is($code, 0, 'a trailing slash on the tree path does not change the result') or diag($output); + +my @damage = ( + ['one changed byte', + sub { write_file("$_[0]/i386/undionly.kpxe", "undi\x00\x02") }, + qr{^content changed: i386/undionly\.kpxe$}m], + ['a missing file', + sub { unlink("$_[0]/x86_64-sb/snponly.efi") or die $! }, + qr{^missing: x86_64-sb/snponly\.efi$}m], + ['an extra file', + sub { write_file("$_[0]/x86_64-sb/extra.efi", 'extra') }, + qr{^unexpected: x86_64-sb/extra\.efi$}m], + ['an extra directory', + sub { make_path("$_[0]/arm64") }, + qr{^unexpected: arm64$}m], + ['a symlink with another target', + sub { unlink("$_[0]/sb") or die $!; symlink('i386', "$_[0]/sb") or die $! }, + qr{^link target changed: sb \(expected x86_64-sb, found i386\)$}m], + ['a symlink replaced by a copy of its target', + sub { unlink("$_[0]/x86_64-sb/ipxe-shim.efi") or die $!; + write_file("$_[0]/x86_64-sb/ipxe-shim.efi", "MZ signed shim") }, + qr{^type changed: x86_64-sb/ipxe-shim\.efi \(expected link, found file\)$}m], +); +my $n = 0; +for my $case (@damage) { + my ($name, $apply, $message) = @{$case}; + my $copy = copy_tree('damaged-' . ++$n); + $apply->($copy); + ($code, $output) = run_checker($copy, $manifest); + is($code, 1, "$name fails the check"); + like($output, $message, "$name is reported by path") or diag($output); +} + +write_file("$tmp/malformed.sha256", "file\tnot-a-digest\ti386/undionly.kpxe\n"); +($code) = run_checker("$tmp/pristine", "$tmp/malformed.sha256"); +is($code, 2, 'a malformed manifest line is an error, not a mismatch'); +write_file("$tmp/duplicate.sha256", "dir\t-\ti386\ndir\t-\ti386\n"); +($code) = run_checker("$tmp/pristine", "$tmp/duplicate.sha256"); +is($code, 2, 'a duplicate manifest path is an error'); +($code) = run_checker("$tmp/pristine/sb", $manifest); +is($code, 2, 'a tree path that is a symlink is refused'); + +# The committed manifest and checksums must describe the committed archives. +my @releases = glob("$pkg_dir/ipxeboot-*.tar.gz"); +is(scalar(@releases), 1, 'the package directory holds one release archive'); +SKIP: { + skip 'no release archive', 3 if @releases != 1; + my $release = "$tmp/release"; + make_path($release); + is(system('tar', '-xzf', $releases[0], '--strip-components=1', '-C', $release), 0, + 'the release archive unpacks'); + ($code, $output) = run_checker($release, "$pkg_dir/payload.sha256"); + is($code, 0, 'payload.sha256 matches the committed release archive') or diag($output); + + open(my $fh, '<', "$pkg_dir/SHA256SUMS") or die "read SHA256SUMS: $!"; + my %sums = map { /^([0-9a-f]{64}) (\S+)$/ ? ($2, $1) : () } <$fh>; + close($fh); + my %actual = map { ($_, Digest::SHA->new(256)->addfile("$pkg_dir/$_", 'b')->hexdigest) } + grep { -f "$pkg_dir/$_" } keys %sums; + is_deeply(\%actual, \%sums, 'SHA256SUMS matches both committed archives'); +} + +done_testing();