11 use English
qw{-no_match_vars
};
12 use Function
::Parameters
;
13 use IPC
::System
::Simple
qw{capture capturex systemx
$EXITVAL EXIT_ANY
};
15 use Test
::BDD
::Cucumber
::StepFile
;
18 use Types
::Path
::Tiny
qw{Path
};
22 use Tails
::IUK
::Utils
qw{run_as_root
};
25 my $bindir = path
(__FILE__
)->parent->parent->parent->parent->child('bin')->absolute;
27 unshift @PATH, $bindir;
29 Given
qr{^a usable temporary directory$}, fun
($c) {
30 my $dirname = Path
::Tiny
->tempdir(CLEANUP
=> 0);
31 $c->{stash
}->{scenario
}->{tempdir
} = $dirname;
32 ok
(defined($dirname));
35 fun inject_new_bootloader_bits_into
($dir) {
36 for (qw{EFI
/BOOT/bootx64
.efi utils
/linux/syslinux
}) {
37 my $injected_file = path
($dir, $_);
38 $injected_file->parent->mkpath;
39 $injected_file->touch;
40 ok
($injected_file->is_file);
44 fun geniso
($srcdir, $outfile) {
45 path
($srcdir, 'isolinux')->mkpath;
46 assert
(path
($srcdir, 'isolinux')->is_dir);
47 path
($srcdir, 'isolinux', 'isolinux.cfg')->spew(
48 "bla\n bli/isolinux/blu\n\n\n bla/isolinux/"
50 assert
(path
($srcdir, 'isolinux', 'isolinux.cfg')->exists);
52 path
($srcdir, 'live')->mkpath;
53 if (! -e path
($srcdir, 'live', 'filesystem.squashfs')) {
54 my $squashfs_tempdir = Path
::Tiny
->tempdir;
55 # an empty SquashFS is invalid
56 path
($squashfs_tempdir, '.placeholder')->touch;
57 capture
("gensquashfs --quiet --pack-dir '$squashfs_tempdir' '$srcdir/live/filesystem.squashfs' 2>/dev/null");
60 "genisoimage --quiet -J -l -cache-inodes -allow-multidot -o '$outfile' '$srcdir' 2>/dev/null");
64 Given
qr{^(an old|a new) ISO image(?: that does not contain file "([^"]+)")?$}, fun
($c) {
65 my $generation = $c->matches->[0] eq 'an old' ?
'old' : 'new';
66 my $file = $c->matches->[1];
67 my $basename = $generation eq 'old' ?
'old.iso' : 'new.iso';
68 my $filename = path
($c->{stash
}->{scenario
}->{tempdir
}, $basename);
69 my $iso_tempdir = Path
::Tiny
->tempdir;
72 ok
(! -e path
($iso_tempdir, $file));
74 inject_new_bootloader_bits_into
($iso_tempdir) if $generation eq 'new';
75 ok
(geniso
($iso_tempdir, $filename));
78 Given
qr{^(an old|a new) ISO image that contains file "([^"]+)"$}, fun
($c) {
79 my $generation = $c->matches->[0];
80 my $file = $c->matches->[1];
81 my $basename = $generation eq 'an old' ?
'old.iso' : 'new.iso';
82 my $filename = path
($c->{stash
}->{scenario
}->{tempdir
}, $basename);
83 my $iso_tempdir = Path
::Tiny
->tempdir;
84 my $file_in_iso = path
($iso_tempdir, $file);
85 $file_in_iso->parent->mkpath();
87 ok
($file_in_iso->is_file);
88 inject_new_bootloader_bits_into
($iso_tempdir) if $generation eq 'new';
89 ok
(geniso
($iso_tempdir, $filename));
92 Given
qr{^(?:two identical ISO images|two ISO images that contain the same set of kernels|two ISO images that contain the same bootloader configuration)$}, fun
($c) {
93 for my $generation (qw{old new
}) {
94 my $basename = $generation.'.iso';
95 my $filename = path
($c->{stash
}->{scenario
}->{tempdir
}, $basename);
96 my $iso_tempdir = Path
::Tiny
->tempdir;
97 path
($iso_tempdir, 'live')->mkpath;
98 inject_new_bootloader_bits_into
($iso_tempdir);
99 geniso
($iso_tempdir, $filename) or croak
"Failed to generate ISO image.";
102 -f path
($c->{stash
}->{scenario
}->{tempdir
}, 'old.iso')
103 && -f path
($c->{stash
}->{scenario
}->{tempdir
}, 'new.iso')
107 Given
qr{^two ISO images when the kernel was upgraded$}, fun
($c) {
108 for my $generation (qw{old new
}) {
109 my $basename = $generation.'.iso';
110 my $filename = path
($c->{stash
}->{scenario
}->{tempdir
}, $basename);
111 my $iso_tempdir = Path
::Tiny
->tempdir;
112 path
($iso_tempdir, 'live')->mkpath;
113 for (qw{vmlinuz initrd
.img
}) {
114 path
($iso_tempdir, 'live', $_)->spew($generation);
116 inject_new_bootloader_bits_into
($iso_tempdir) if $generation eq 'new';
117 geniso
($iso_tempdir, $filename) or croak
"Failed to generate ISO image.";
121 -f path
($c->{stash
}->{scenario
}->{tempdir
}, 'old.iso')
122 && -f path
($c->{stash
}->{scenario
}->{tempdir
}, 'new.iso')
126 Given
qr{^two ISO images when a new kernel was added$}, fun
($c) {
127 for my $generation (qw{old new
}) {
128 my $basename = $generation.'.iso';
129 my $filename = path
($c->{stash
}->{scenario
}->{tempdir
}, $basename);
130 my $iso_tempdir = Path
::Tiny
->tempdir;
131 path
($iso_tempdir, 'live')->mkpath;
132 for (qw{vmlinuz initrd
.img
}) {
133 path
($iso_tempdir, 'live', $_)->spew("same content");
135 if ($generation eq 'new') {
136 for (qw{vmlinuz2 initrd2
.img
}) {
137 path
($iso_tempdir, 'live', $_)->spew($generation);
139 inject_new_bootloader_bits_into
($iso_tempdir);
141 geniso
($iso_tempdir, $filename) or croak
"Failed to generate ISO image.";
145 -f path
($c->{stash
}->{scenario
}->{tempdir
}, 'old.iso')
146 && -f path
($c->{stash
}->{scenario
}->{tempdir
}, 'new.iso')
150 Given
qr{^(an old|a new) ISO image whose filesystem.squashfs( does not|) contains? file "([^"]+)"(?:| modified at ([0-9]+)| owned by ([a-z-]+))$}, fun
($c) {
151 my $generation = $c->matches->[0] eq 'an old' ?
'old' : 'new';
152 my $contains = $c->matches->[1] eq "" ?
1 : 0;
153 my $file = $c->matches->[2];
155 if (defined $c->matches->[3] && $c->matches->[3] =~ m{\A[0-9]+\z}) {
156 $mtime = $c->matches->[3];
157 } elsif (defined $c->matches->[4] && $c->matches->[4] =~ m{\A[a-z-]+\z}) {
158 $owner = $c->matches->[4];
161 my $iso_basename = $generation eq 'old' ?
'old.iso' : 'new.iso';
162 my $iso_filename = path
($c->{stash
}->{scenario
}->{tempdir
}, $iso_basename);
163 my $iso_tempdir = Path
::Tiny
->tempdir;
164 my $squashfs_tempdir = Path
::Tiny
->tempdir;
165 # an empty SquashFS is invalid
166 path
($squashfs_tempdir, '.placeholder')->touch;
168 path
($squashfs_tempdir, $file)->parent->mkpath();
169 path
($squashfs_tempdir, $file)->touch;
170 utime($mtime, $mtime, path
($squashfs_tempdir, $file)) if defined($mtime);
171 run_as_root
('chown', $owner, path
($squashfs_tempdir, $file)) if defined($owner);
173 path
($iso_tempdir, 'live')->mkpath();
174 systemx
('sudo', 'chmod', '-R', 'go+rwX', $squashfs_tempdir);
176 'gensquashfs', '--quiet', '--keep-time',
177 '--pack-dir', $squashfs_tempdir,
178 $iso_tempdir->child('live/filesystem.squashfs')
180 inject_new_bootloader_bits_into
($iso_tempdir) if $generation eq 'new';
181 ok
(geniso
($iso_tempdir, $iso_filename));
184 Given
qr{^two ISO images that do not contain the same bootloader configuration$}, fun
($c) {
185 for my $generation (qw{old new
}) {
186 my $basename = $generation.'.iso';
187 my $filename = path
($c->{stash
}->{scenario
}->{tempdir
}, $basename);
188 my $iso_tempdir = Path
::Tiny
->tempdir;
189 path
($iso_tempdir, 'isolinux')->mkpath;
191 path
($iso_tempdir, 'isolinux', $_)->spew($generation);
193 inject_new_bootloader_bits_into
($iso_tempdir) if $generation eq 'new';
194 geniso
($iso_tempdir, $filename) or croak
"Failed to generate ISO image.";
198 -f path
($c->{stash
}->{scenario
}->{tempdir
}, 'old.iso')
199 && -f path
($c->{stash
}->{scenario
}->{tempdir
}, 'new.iso')
203 When
qr{^I create an IUK$}, fun
($c) {
205 $c->{stash
}->{scenario
}->{squashfs_diff_name
} =
206 'Tails_amd64_0.11.1_to_0.11.2.squashfs';
208 my $iuk_path = path
($c->{stash
}->{scenario
}->{tempdir
}, 'test.iuk');
211 # on overlayfs, deleted files are stored using character devices,
212 # that one needs to be root to create
213 "sudo SOURCE_DATE_EPOCH=$ENV{SOURCE_DATE_EPOCH} " .
214 path
($bindir, "tails-create-iuk") .
216 path
($c->{stash
}->{scenario
}->{tempdir
}, 'old.iso') . '" ' .
218 path
($c->{stash
}->{scenario
}->{tempdir
}, 'new.iso') . '"' .
219 ' --squashfs_diff_name "'.$c->{stash
}->{scenario
}->{squashfs_diff_name
}.'"' .
220 ' --outfile "' . $iuk_path . '"'
223 if (exists $c->{stash
}->{scenario
}->{squashfs_diff
}) {
225 ' --squashfs_diff "' . $c->{stash
}->{scenario
}->{squashfs_diff
} . '"';
228 $c->{stash
}->{scenario
}->{create_output
} = capture
(
230 "umask 077 && $cmdline 2>&1"
232 $c->{stash
}->{scenario
}->{create_exit_code
} = $EXITVAL;
234 $c->{stash
}->{scenario
}->{create_exit_code
} == 0
235 or warn $c->{stash
}->{scenario
}->{create_output
};
237 $c->{stash
}->{scenario
}->{iuk_path
} = $iuk_path;
239 my $iuk_in = Tails
::IUK
::Read
->new_from_file($iuk_path);
241 my @gids = split(/ /, $GID);
242 system(qw{sudo
chown}, "$UID:$gids[0]", $iuk_path);
243 ${^CHILD_ERROR_NATIVE
} == 0 or croak
"Could not chown '$iuk_path': $!";
245 $c->{stash
}->{scenario
}->{iuk_in
} = $iuk_in;
246 ok
(defined($iuk_in));
249 Then
qr{^the created IUK is a SquashFS image$}, fun
($c) {
250 system('rdsquashfs --list / ' . $c->{stash
}->{scenario
}->{iuk_path
} . '>/dev/null 2>&1');
251 is
(${^CHILD_ERROR_NATIVE
}, 0, "The generated IUK is not a SquashFS image");
254 Then
qr{^the saved IUK contains a "([^"]*)" file$}, fun
($c) {
255 my $file = path
($c->matches->[0]);
256 ok
($c->{stash
}->{scenario
}->{iuk_in
}->contains_file($file));
259 fun squashfs_contains_only_files_owned_by
($squashfs_filename, $owner, $group) {
263 \A
# at the beginning of the string
264 (?
:file
|dir
) # file type
268 [[:digit
:]]+ # permissions
274 "line looks like a file description with owner $owner and group $group"
275 ) } split(/\n/, `rdsquashfs --quiet --describe '$squashfs_filename'`);
278 Then
qr{^all files in the saved IUK belong to owner 0 and group 0$}, fun
($c) {
279 ok
(squashfs_contains_only_files_owned_by
(
280 $c->{stash
}->{scenario
}->{iuk_in
}->file,
286 Then
qr{^the "([^"]+)" file in the saved IUK contains "([^"]*)"$}, fun
($c) {
287 my $file = $c->matches->[0];
288 my $expected_content = $c->matches->[1];
290 $c->{stash
}->{scenario
}->{iuk_in
}->get_content(path
($file)),
295 fun _file_content_in_iuk_like
(
296 $iuk_in, Path
$filename, $regexp, $should_match
298 assert
(defined $iuk_in && defined $filename);
299 assert
(defined $regexp && defined $should_match);
301 unless ($iuk_in->contains_file($filename)) {
302 warn "The IUK does not contain $filename, so we can't check its content.";
306 my $content = $iuk_in->get_content($filename);
309 return $content =~ m{$regexp}xms;
312 return $content !~ m{$regexp}xms;
316 fun file_content_in_iuk_like
($iuk_in, Path
$filename, $regexp) {
317 assert
(defined $iuk_in && defined $regexp);
318 _file_content_in_iuk_like
(@_, 1);
321 fun file_content_in_iuk_unlike
($iuk_in, Path
$filename, $regexp) {
322 assert
(defined $iuk_in && defined $regexp);
323 _file_content_in_iuk_like
(@_, 0);
326 fun squashfs_in_iuk_contains
(:$iuk_in, :$squashfs_name, :$expected_file,
327 :$expected_mtime, :$expected_owner) {
328 my $squashfs_path = path
('overlay', 'live', $squashfs_name);
329 die "SquashFS '$squashfs_name' not found in the IUK"
330 unless $iuk_in->contains_file($squashfs_path);
332 my $orig_cwd = getcwd
;
333 my $tempdir = Path
::Tiny
->tempdir;
335 $tempdir->child('squashfs-root')->mkpath;
337 # on overlayfs, deleted files are stored using character devices,
338 # that one needs to be root to create
340 'rdsquashfs', '--quiet', '--set-times', '--set-xattr', '--chown',
341 '--unpack-root', $tempdir->child('squashfs-root'),
342 '--unpack-path', "/",
343 $iuk_in->mountpoint->child($squashfs_path),
345 my $exists = $tempdir->child('squashfs-root', $expected_file)->exists;
348 # Ensure $tempdir can be cleaned up and the $expected_mtime test can access
349 # the file it needs to
350 my @gids = split(/ /, $GID);
352 qw{sudo
chmod -R go
+rwX
},
353 $tempdir->child('squashfs-root')
356 return unless $exists;
358 if (defined $expected_mtime) {
359 $expected_mtime = $ENV{SOURCE_DATE_EPOCH
} if $expected_mtime eq 'SOURCE_DATE_EPOCH';
360 return unless $expected_mtime == $tempdir->child('squashfs-root', $expected_file)->stat->mtime
363 if (defined $expected_owner) {
364 return unless $expected_owner eq getpwuid($tempdir->child('squashfs-root', $expected_file)->stat->uid)
370 fun squashfs_in_iuk_deletes
($iuk_in, $squashfs_name, $deleted_file) {
371 my $squashfs_path = path
('overlay', 'live', $squashfs_name);
372 my $orig_cwd = getcwd
;
373 my $tempdir = Path
::Tiny
->tempdir;
375 die "SquashFS '$squashfs_name' not found in the IUK"
376 unless $iuk_in->contains_file($squashfs_path);
378 my $old_dir = Path
::Tiny
->tempdir;
379 path
($old_dir, $deleted_file)->touch;
381 my $new_dir = Path
::Tiny
->tempdir;
383 # on overlayfs, deleted files are stored using character devices,
384 # that one needs to be root to create
386 "rdsquashfs", '--quiet', "--unpack-root", $new_dir,
387 '--unpack-path', '.',
388 $iuk_in->mountpoint->child($squashfs_path),
392 # Ensure $new_dir can be cleaned up
393 my @gids = split(/ /, $GID);
394 systemx
(qw{sudo
chown -R
}, "$UID:$gids[0]", $new_dir);
396 my $union_basedir = Path
::Tiny
->tempdir;
397 my $union_workdir = path
($union_basedir, 'work');
398 my $union_mountpoint = path
($union_basedir, 'mount');
399 $_->mkpath for ($union_workdir, $union_mountpoint);
402 '-o', sprintf("noatime,lowerdir=%s,upperdir=%s,workdir=%s",
403 $old_dir, $new_dir, $union_workdir),
408 qw{sudo
-n mount
}, @mount_args,
412 my $exists = -e path
($union_mountpoint, $deleted_file);
413 system(qw{sudo umount
}, "$union_mountpoint");
417 Then
qr{^the saved IUK contains an? "([^"]+)" directory$}, fun
($c) {
418 my $dir = $c->matches->[0];
419 ok
($c->{stash
}->{scenario
}->{iuk_in
}->mountpoint->child($dir)->is_dir);
422 fun overlay_directory_in_iuk_contains
($iuk_in, $expected_file) {
423 grep { 'overlay/' . $expected_file eq $_->stringify } $iuk_in->list_files;
426 Then
qr{^the overlay directory in the saved IUK (contains|does not contain) "([^"]+)"$}, fun
($c) {
427 my $should_contain = $c->matches->[0] eq 'contains';
428 my $expected_file = $c->matches->[1];
431 ok
(overlay_directory_in_iuk_contains
(
432 $c->{stash
}->{scenario
}->{iuk_in
},
436 ok
(! overlay_directory_in_iuk_contains
(
437 $c->{stash
}->{scenario
}->{iuk_in
},
442 Then
qr{^the delete_files list (contains|does not contain) "([^"]+)"$}, fun
($c) {
443 my $wanted = $c->matches->[0] eq 'contains' ?
1 : 0;
444 my $expected_filename = $c->matches->[1];
448 $_ eq $expected_filename
449 } @
{$c->{stash
}->{scenario
}->{iuk_in
}->delete_files}),
454 Then
qr{^the delete_files list is empty$}, fun
($c) {
455 is
($c->{stash
}->{scenario
}->{iuk_in
}->delete_files_count, 0);
458 Then
qr{^the saved IUK contains a SquashFS that contains file "([^"]+)"(?:| modified at ([0-9]+|SOURCE_DATE_EPOCH)| owned by ([a-z-]+))$}, fun
($c) {
459 my $expected_file = $c->matches->[0];
460 my ($expected_mtime, $expected_owner);
461 if (defined $c->matches->[1] && $c->matches->[1] =~ m{\A[0-9]+\z}) {
462 $expected_mtime = $c->matches->[1];
463 } elsif (defined $c->matches->[1] && $c->matches->[1] eq "SOURCE_DATE_EPOCH") {
464 $expected_mtime = $c->matches->[1];
465 } elsif (defined $c->matches->[2] && $c->matches->[2] =~ m{\A[a-z-]+\z}) {
466 $expected_owner = $c->matches->[2];
468 croak
"Test suite implementation error";
471 ok
(squashfs_in_iuk_contains
(
472 iuk_in
=> $c->{stash
}->{scenario
}->{iuk_in
},
473 squashfs_name
=> $c->{stash
}->{scenario
}->{squashfs_diff_name
},
474 expected_file
=> $expected_file,
475 expected_mtime
=> $expected_mtime,
476 expected_owner
=> $expected_owner,
480 Then
qr{^the overlay directory in the saved IUK contains a SquashFS diff$}, fun
($c) {
481 ok
(overlay_directory_in_iuk_contains
(
482 $c->{stash
}->{scenario
}->{iuk_in
},
483 path
('live', $c->{stash
}->{scenario
}->{squashfs_diff_name
}),
487 Then
qr{^the saved IUK contains a SquashFS that deletes file "([^"]+)"$}, fun
($c) {
488 my $deleted_file = $c->matches->[0];
490 ok
(squashfs_in_iuk_deletes
(
491 $c->{stash
}->{scenario
}->{iuk_in
},
492 $c->{stash
}->{scenario
}->{squashfs_diff_name
},
497 Then
qr{^the saved IUK contains the new bootloader configuration$}, fun
($c) {
498 my $live_cfg_path = path
('overlay/syslinux/live.cfg');
500 $c->{stash
}->{scenario
}->{iuk_in
}->get_content($live_cfg_path),
505 Then
qr{^the overlay directory contains an upgraded syslinux configuration$}, fun
($c) {
506 my @wanted_files = qw{syslinux
/syslinux.cfg EFI/BOOT
/bootx64.efi utils/linux
/syslinux
};
507 my @unwanted_files = qw{syslinux
/isolinux
.cfg
};
510 ok
(overlay_directory_in_iuk_contains
(
511 $c->{stash
}->{scenario
}->{iuk_in
}, $_,
512 ), "the overlay directory contains '$_'");
516 ok
(! overlay_directory_in_iuk_contains
(
517 $c->{stash
}->{scenario
}->{iuk_in
}, $_,
518 ), "the overlay directory does not contain '$_'");
521 ok
(file_content_in_iuk_like
(
522 $c->{stash
}->{scenario
}->{iuk_in
},
523 path
("overlay/syslinux/syslinux.cfg"),
525 ), "overlay/syslinux/syslinux.cfg contains /syslinux/");
527 ok
(file_content_in_iuk_unlike
(
528 $c->{stash
}->{scenario
}->{iuk_in
},
529 path
("overlay/syslinux/syslinux.cfg"),
531 ), "overlay/syslinux/syslinux.cfg does not contain /isolinux/");