1
0
forked from rosa/hakurei

Compare commits

...

407 Commits

Author SHA1 Message Date
cat 5992be3dd3 internal/rosa/package/python: add kernel-headers
Missing kernel headers implicitly disables many useful things. This change also enables PGO and installs symlink, since a rebuild is unavoidable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-10 16:30:52 +09:00
cat c661a3b63a cmd/mbf: migrate shell to enter
This reduces duplicate code. This change also adds resolv.conf to the container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-10 13:34:44 +09:00
cat df0bb877db internal/rosa: export etc native artifact
This is useful for external container tooling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-10 13:26:38 +09:00
cat f333b8fbd6 cmd/mbf: register binfmt entry for shell
This fixes --arch for shell.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-10 12:51:09 +09:00
cat 928a9f61e9 cmd/hsu: remove parent check
This check serves no real purpose and only makes it more difficult to start containers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-09 18:08:17 +09:00
cat ce06539eca internal/rosa: expose supported architectures
This information is useful to external tooling and makes a lot more sense in this package than cmd/mbf. This change also fixes non-native artifact resolution during clean.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-09 16:05:04 +09:00
cat 69908e5a41 internal/rosa: remove external toolchain reference
This should have used toolchain passed as the argument and was mistakenly never changed when migrating to late evaluation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-09 15:58:02 +09:00
cat b5445573a8 internal/rosa/package/ninja: work around test suite bug
The test suite hard codes /bin/echo.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-08 18:43:42 +09:00
cat f869ff95a1 all: apply modernisers
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-08 14:58:24 +09:00
cat 725f2e0ef3 internal/rosa/package/wayland: wayland-protocols 1.48 to 1.49
This finally eliminates the backport patch.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-08 13:45:36 +09:00
cat 4ffa20cd3f cmd/mbf: custom azalea path
This enables out-of-tree packaging.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-08 13:42:06 +09:00
cat 38450db74a internal/rosa: access backing storage through fs
This is more versatile than hardcoding the os.Root implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 22:08:57 +09:00
cat 9344f694c7 internal/pkg: defer directory permissions
This allows creation of directory structures with awkward permission bits.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 20:26:55 +09:00
cat e8bb5a622d internal/rosa: mirror service via external cache
This provides an authenticated implementation of the external cache.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 19:25:11 +09:00
cat 22e508fe17 internal/pkg: expose measured reader to extern status
This is always required by the implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 16:14:54 +09:00
cat b18ecf5832 nix: work around systemd nondeterminism
NixOS switched to systemd initramfs by default.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 14:10:06 +09:00
cat ec29e755fb internal/pkg: replace outcomes from external cache
This is primarily useful for implementing a mirror service. This change also works around the zero-dependency FloodArtifact edge case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 13:52:26 +09:00
cat 9aaf160ff4 internal/pkg: do not hold up cures during status link
This is a bunch of string operation and a single syscall. Do not hold up queue here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 02:28:45 +09:00
cat e192fca762 internal/pkg: check for unclean shutdown
This avoids running into nasty surprises opening a cache that suffered unclean shutdown due to power loss. All other parts of the cache are not prone to inconsistent state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 01:21:00 +09:00
cat 7eafc7b1e4 nix: update flake lock
Was unfortunately not able to implement vm test suite before this release. Hopefully the last nixos update we have to follow.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-06 13:14:09 +09:00
cat 282462c2f0 internal/rosa/package/mesa: 26.1.1 to 26.1.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:23:40 +09:00
cat d9f522d648 internal/rosa/package/libinput: 1.31.2 to 1.31.3
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:23:11 +09:00
cat a0b0c7ecc9 internal/rosa/package/libdrm: 2.4.133 to 2.4.134
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:22:50 +09:00
cat 2b8809da7a internal/rosa/package/hwdata: 0.407 to 0.408
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:22:28 +09:00
cat 84cda19d63 internal/rosa/package/harfbuzz: 14.2.0 to 14.2.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:22:08 +09:00
cat 154ab953c1 internal/rosa/package/fontconfig: 2.18.0 to 2.18.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:21:43 +09:00
cat 34e11dd312 internal/rosa/package/python: hatchling 1.16.5 to 1.17.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:21:10 +09:00
cat 140cb3cc47 internal/rosa/package/python: trove-classifiers 2026.5.22.10 to 2026.6.1.19
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:20:42 +09:00
cat 82c974d656 internal/rosa/package/xkbcommon: 1.13.1 to 1.13.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:20:12 +09:00
cat 8c17f201e5 internal/rosa/package/x: xwayland 24.1.11 to 24.1.12
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:19:46 +09:00
cat 63e0457538 internal/rosa/package/x: xserver 21.1.22 to 21.1.23
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:19:22 +09:00
cat ca93264c1f internal/rosa/package/dtc: 1.8.0 to 1.8.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:18:53 +09:00
cat 94173eafff internal/rosa/llvm: 22.1.6 to 22.1.7
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 20:13:26 +09:00
cat e28c4aa3c0 internal/rosa/package/kernel: 6.12.91 to 6.12.92
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 20:10:27 +09:00
cat 8e8410ce38 internal/pkg: archive unpack artifact
This unpacks an internal/pkg archive stream used in the upcoming mirror service.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 19:46:36 +09:00
cat 8fb6fdaa80 internal/pkg: prefix reporting names
This reads better than suffixes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 19:05:36 +09:00
cat db69dcf0be internal/pkg: remove tar built-in decompressor
This is replaced by decompressArtifact and is no longer necessary.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 18:54:43 +09:00
cat 76c1fb84c8 internal/pkg: stream decompress artifact
The tarArtifact predates FileArtifact pipelining. This migrates decompression and buffering into a standalone artifact implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 18:33:04 +09:00
cat 729be19af3 internal/pkg: rename archive checksum helpers
These names are more consistent with other helper names, so rename them while the API is still internal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 14:13:20 +09:00
cat 4d04f86f4a internal/rosa/go: 1.26.3 to 1.26.4
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-03 16:18:04 +09:00
cat 42cea1e7c6 internal/pkg: streaming archive reader/writer
This is much more robust and efficient than the simple buffering implementation for larger files. Allocations happen almost exclusively in WalkDir.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-03 16:00:36 +09:00
cat 83498b5a8a cmd/mbf: keep non-native entries alive
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 16:06:04 +09:00
cat 9e824452bd internal/pkg: expose snapshot of binfmt entries
This is otherwise not externally accessible. The resulting map can be safely mutated.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 16:05:09 +09:00
cat 56937ac396 internal/rosa: populate opts of cloned S
This was missed when migrating opts into S.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 16:00:51 +09:00
cat 6c2f7089b6 internal/pkg: destroy unreachable status entries
These must be destroyed alongside their corresponding identifier or substitute entries to avoid inconsistent state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 15:50:30 +09:00
cat 74c18390b4 internal/pkg: correctly scrub substitute status
Scrubbing for status predates substitutes. This change fixes scrub handling of substitute status entries.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 15:37:55 +09:00
cat 1490b32387 cmd/mbf: garbage collection commands
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 13:37:46 +09:00
cat fbd2329d50 internal/pkg: garbage collection
This destroys cache entries not referred to by user-specified artifacts and optionally their inputs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 13:28:24 +09:00
cat f398f71fa9 internal/pkg: input iterator via IR cache
Primarily useful for garbage collection.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-31 14:19:27 +09:00
cat 4d017b1309 internal/rosa/package/rsync: annotate blocked update
Despite rsync being practically feature complete, with minimal changes in many years, it recently had a large number of poor quality AI-generated changes, with the first release including them apparently being 3.4.2. Release 3.4.3 then introduced a dependency on kernel-headers, creating a dependency loop that prevented directly upgrading to it. This change annotates the blockage since it is not worth working around the dependency loop for AI-generated garbage.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-30 23:15:04 +09:00
cat cd1d447664 internal/rosa/package/rsync: 3.4.2 to 3.4.1
This appears to be the last release not to contain AI slop.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-30 19:10:40 +09:00
cat 8b87e0eb76 internal/rosa: annotate blocked updates
These situations, while unfortunate, are inevitable at a larger scale. This change enables annotation and optional hiding of blocked updates.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-30 18:56:55 +09:00
cat f4215ddda5 internal/rosa/package/qemu: 11.0.0 to 11.0.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-28 12:16:55 +09:00
cat 652b4c2ba0 internal/rosa/package/gnu: parallel 20260422 to 20260522
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-28 12:16:05 +09:00
cat 75715f4590 cmd/earlyinit: mount /dev/shm
Required by many programs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 23:59:48 +09:00
cat 87c3b3663d cmd/earlyinit: improve change message formatting
This is significantly more readable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 23:51:46 +09:00
cat 6d792023b2 internal/rosa/package: seatd
Required by wlroots sessions.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 23:17:33 +09:00
cat 3e62cf379f internal/rosa/package: libliftoff
Wanted by wlroots.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 23:07:53 +09:00
cat 6d991f2644 cmd/earlyinit: downgrade target error severity
This happens during coldboot, before reporting gains persistent state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 18:58:30 +09:00
cat a2cc28f53c cmd/earlyinit: load device drivers
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 18:54:05 +09:00
cat 598c7aa30f internal/report: strict-exempt severity
For reportable but inconsequential errors like modprobe by MODALIAS failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 18:53:15 +09:00
cat b18f40d974 internal/kobject: pass action kind for range
Useful for handling most uevents.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 17:58:01 +09:00
cat 12a6061051 internal/rosa/package: sway
Required by vm test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 17:10:18 +09:00
cat 02c3823ed7 internal/rosa/package: libinput
Required by sway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 17:07:49 +09:00
cat cfbd8bd6f1 internal/rosa/package: libudev-zero
Avoids systemd, required by many projects.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 17:07:35 +09:00
cat fc7a339ed2 internal/rosa/package/kernel: 6.12.90 to 6.12.91
This unfortunately updates headers linux/mii.h and linux/virtio_net.h.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 15:47:53 +09:00
cat 97fdd5db8b internal/rosa/package/dtc: 1.7.2 to 1.8.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 15:46:58 +09:00
cat a5d9f76f50 internal/rosa/cmake: use DESTDIR instead of --prefix
Turns out --prefix is deeply broken, and DESTDIR works even when using ninja.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 15:45:58 +09:00
cat b313dfefb0 internal/rosa/package: libevdev
Required by sway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 13:07:10 +09:00
cat 5b32297178 internal/rosa/package: check
Required by libevdev.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 13:05:24 +09:00
cat 32cc72821e internal/rosa/package: pango
Required by sway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 12:33:10 +09:00
cat 958473e6f4 internal/rosa/package: harfbuzz
Required by pango.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 12:32:59 +09:00
cat cc52bb3035 internal/rosa/package: fribidi
Required by pango.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 12:13:31 +09:00
cat 7816f8b523 internal/rosa/package: cairo
Required by sway, which is required by vm test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 12:00:13 +09:00
cat 91e4229e32 internal/rosa/package: json-c
Required by sway, which is required by vm test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 11:20:28 +09:00
cat c346cb4c57 internal/rosa/package: wlroots
Required by sway, which is required by vm test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 10:59:09 +09:00
cat 0e0d5e8def internal/rosa/package/x: xwayland
Generally useful for wayland display servers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 10:45:03 +09:00
cat ea875416d4 internal/rosa/package: enable libglvnd in libepoxy
This is no longer circular because xserver does not require libepoxy.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 10:38:22 +09:00
cat 8c7109a2d5 cmd/earlyinit: mount system device
This currently relies on a trusted bootloader to determine the boot device.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 10:37:45 +09:00
cat caf3e0db4b internal/kobject: improve error JSON representation
This is now usable by internal/report.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-26 18:39:09 +09:00
cat 12b9f51128 internal/report: report errors with persistent backing
This is useful for reporting errors from processes that must never terminate.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-26 14:56:16 +09:00
cat d15f965d0c internal/kobject: range over objects
For matching devices based on some criteria.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-25 23:17:14 +09:00
cat fadd1b14f7 internal/kobject: process uevent
This tracks kernel state by merging a stream of uevent. Inconsistencies are reported and recovered from gracefully.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-25 21:31:59 +09:00
cat 3458806685 internal/rosa: resolve runtime for overlay extras
This is generally for system image creation, so this behaviour makes more sense.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-25 10:37:21 +09:00
cat 8ca70550ab internal/rosa/package: db
For iproute2.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-25 10:31:45 +09:00
cat 7eebf49b98 internal/rosa/package: libbpf
For eBPF support in iproute2.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-25 09:51:26 +09:00
cat 8c84883af6 internal/rosa/package: iproute2
Useful for debugging for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-25 09:33:32 +09:00
cat 4e1359aa95 internal/rosa/package: ignore local directory
For testing packages noneligible for upstream.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-24 16:47:24 +09:00
cat a3867ad65f internal/rosa/azalea: replace binding token
This replaces the '*' placeholder with a less confusing '#'.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-24 16:20:26 +09:00
cat 689f972976 internal/rosa/package: migrate stage0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 17:53:05 +09:00
cat 3f33b62dfd internal/rosa/package: migrate system image
The overlay argument also enables migration of stage0.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 17:44:38 +09:00
cat ac5488eef6 internal/rosa/package: migrate initramfs image
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 17:30:39 +09:00
cat 77a15130c7 internal/rosa/package: foot
Used by vm test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 16:51:32 +09:00
cat 4c1e823908 internal/rosa/package: fcft
Required by foot.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 16:45:42 +09:00
cat 5f5a398a5b internal/rosa/package: utf8proc
Required by foot.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 16:35:38 +09:00
cat d5e4a2e6a7 internal/rosa/package: tllist
Required by foot.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 16:35:24 +09:00
cat 57c6b84b60 internal/rosa/package: fontconfig
Required by foot.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 16:23:06 +09:00
cat 4269627b4b internal/rosa/package: xkbcommon
Required by foot.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 16:22:51 +09:00
cat d50e3c3d5b internal/rosa/package/glib: 2.88.1 to 2.89.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 14:33:37 +09:00
cat eae2890d98 internal/rosa/package/python: trove-classifiers 2026.5.7.17 to 2026.5.22.10
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 14:33:20 +09:00
cat f8ebfd71a7 internal/rosa/package/spirv: spirv-headers 1.4.341.0 to 1.4.350.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 14:32:52 +09:00
cat dce1a05f6c internal/rosa/package/firmware: 20260410 to 20260519
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 14:32:16 +09:00
cat eff265837c internal/rosa/package/mesa: 26.1.0 to 26.1.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 14:32:01 +09:00
cat 7d809eb15f internal/rosa/package/nss: 3.123.1 to 3.124
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 14:31:36 +09:00
cat 9accc2f961 internal/rosa/package/nss: nspr 4.38.2 to 4.39
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 14:31:19 +09:00
cat e5a4094298 internal/rosa: remove unused helpers
These are no longer needed after migration.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 14:00:18 +09:00
cat 410c4f8bb0 internal/rosa/package/cmake: 4.3.2 to 4.3.3
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 13:02:23 +09:00
cat 2e23c6d367 internal/rosa/package/kernel: 6.12.87 to 6.12.90
Unfortunately this causes rebuilds due to a single io_uring API change.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 13:01:44 +09:00
cat f5e9a0c04e internal/pkg: destroy new substitution status on fault
This avoids leaving behind the substitution status path of a faulted cure.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 01:31:13 +09:00
cat 3bd4ef616c internal/pkg: report errors exiting cure
This makes ongoing errors more obvious when multiple failures occur.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 01:25:42 +09:00
cat 41402fd578 internal/rosa/package/util-linux: 2.42 to 2.42.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-23 01:00:27 +09:00
cat b47fa1a214 internal/rosa: IR-curable source override
This creates a tarball in-memory for overriding hakurei-source.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-22 22:24:16 +09:00
cat 9e363cb2c9 internal/rosa/go: runtime dependencies for alterative path
The GCC toolchain is not dependency-free, so append them here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-22 13:42:02 +09:00
cat 1389c77022 internal/rosa/package/hakurei: 0.4.2 to 0.4.3
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-22 03:16:56 +09:00
cat e231341e48 release: 0.4.3
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-22 02:18:10 +09:00
cat 70f977627d internal/pkg: arch-specific expected offline substituted
IR includes the target architecture name.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-22 01:51:15 +09:00
cat f3a6f7ddf9 internal/rosa/package: migrate hakurei
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-22 00:07:25 +09:00
cat 2a9aa3b400 cmd/dist: include version in release
This makes HAKUREI_VERSION optional during build.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 23:37:08 +09:00
cat 68a91523b9 internal/rosa/package: migrate bison
This is the final remaining trivial legacy artifact.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 23:06:34 +09:00
cat 5647321622 internal/rosa: move azalea builtins
This improves readability.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 22:51:50 +09:00
cat cdd31dd27b internal/rosa/azalea: integer arrays
This is useful for some helper functions. Performance is unaffected.

Before:
BenchmarkStage3-128     	    8308	   1960687 ns/op	 1023794 B/op	   14755 allocs/op
BenchmarkAll-128        	    3331	   5518571 ns/op	 2902320 B/op	   37993 allocs/op

After:
BenchmarkStage3-128     	    8330	   1946273 ns/op	 1023046 B/op	   14750 allocs/op
BenchmarkAll-128        	    3296	   5585805 ns/op	 2901746 B/op	   37991 allocs/op

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 22:47:41 +09:00
cat 0615899e56 internal/rosa: do not register stage0
Nothing can depend on this, so remove it from the namespace.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 22:20:01 +09:00
cat 0914569e62 internal/rosa/go: migrate to generic helper
The go toolchain predates all abstractions currently available. This migration causes rebuilds due to internal cleanups affecting the final build script.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 19:55:38 +09:00
cat 25d9edfc64 internal/rosa/package: migrate tamago
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 19:40:16 +09:00
cat af4c3bbff2 internal/rosa/package: migrate toybox
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 18:10:02 +09:00
cat 54aae9d72a internal/rosa/package: migrate llvm patches
LLVM itself is unlikely to ever be migrated due to complexity of the bootstrap, so migrate patches instead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 17:51:23 +09:00
cat 58646f8ea5 internal/rosa/package/googletest: 1.16.0 to 1.17.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 17:02:40 +09:00
cat 9d7a27d8ac internal/rosa/package: migrate ninja
The ninja package predates all abstractions currently available. This migration causes rebuilds due to the old package being nonreproducible.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 16:48:10 +09:00
cat 6546ddc64b internal/rosa: expose in-place behaviour in generic helper
This change also combines the createDir and wantsDir methods, and replaces the non-inplace target of the generic helper with a deterministic path.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 16:11:38 +09:00
cat cbf18b302d internal/rosa/package: migrate nss
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 15:36:10 +09:00
cat 1acb5b0105 internal/rosa: extra inputs in alternative path
This works around particularly unwieldy build systems.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 15:05:12 +09:00
cat 40b33f9fc7 internal/rosa: enforce exclusions
This restores unexported artifact behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 14:48:16 +09:00
cat 443a7a30f6 internal/rosa: use string pair for files
This is a much cleaner representation than the separator syntax.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 14:07:20 +09:00
cat 497e4a5642 internal/rosa/package/git: disable flaky test
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 13:35:21 +09:00
cat c0e3841ddb internal/rosa/llvm: 22.1.5 to 22.1.6
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-21 10:39:42 +09:00
cat 9ce2c325db internal/rosa/package/kernel: populate riscv64 checksum
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 21:25:15 +09:00
cat 9836030c59 internal/rosa: reinitialise frame alongside cache
The cached frame can contain information made outdated by the DropCaches call.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 09:01:40 +09:00
cat b482fd4abf internal/rosa: remove global handles
These no longer serve any purpose.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 08:15:23 +09:00
cat 2e502ede6c internal/rosa/package: migrate X packages
This also improves naming consistency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 07:44:17 +09:00
cat 4bec0b890c internal/rosa/package: migrate unzip
This is now possible via the generic helper.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 06:21:49 +09:00
cat 7770ccf0aa internal/rosa/package: migrate wayland
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 06:12:19 +09:00
cat 656059278d internal/rosa/package: migrate remaining trivial packages
The rest are migrated individually.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 05:57:26 +09:00
cat 1a9974ffdc internal/rosa/package: migrate qemu
This has many dependencies.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 05:10:41 +09:00
cat 1a2699b486 internal/rosa/package: migrate multiple packages
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 05:01:06 +09:00
cat 1d3d621e2f internal/rosa/package: migrate perl Module::Build
This is now possible via the generic helper.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 04:48:03 +09:00
cat 47f4e287fc internal/rosa/package: migrate multiple libraries
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 04:37:58 +09:00
cat 2e710328a4 internal/rosa/package: migrate musl
This removes some legacy cruft, causing 2 rebuilds per stage.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 04:13:10 +09:00
cat 2e7b52d701 internal/rosa/package: migrate mesa
This has many dependencies.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 04:02:02 +09:00
cat d728607505 internal/rosa/package: migrate mesa dependencies
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 03:51:48 +09:00
cat ef414ab01a internal/rosa/package: migrate many libraries
This also adds more string helpers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 03:33:15 +09:00
cat 96abf266dd internal/rosa/package: migrate hwdata, kmod, libarchive
This removes a blank line in CTestCustom.cmake, causing a libarchive rebuild. Resulting IR is identical otherwise.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 02:25:41 +09:00
cat fcba32e9c4 internal/rosa/package: migrate glib
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 02:09:12 +09:00
cat a7f5a5802d internal/rosa/package: migrate spirv-llvm-translator
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-20 00:01:03 +09:00
cat bb230378e0 internal/rosa/package: migrate glslang
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 23:48:39 +09:00
cat f638c73933 internal/rosa: bind anitya functions
This is far more scalable than individual fields.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 23:34:21 +09:00
cat 98d915af3d internal/rosa/package: migrate argp-standalone, dtc, elfutils, flex, freetype, fuse
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 23:26:34 +09:00
cat c0593e8325 internal/rosa/package: migrate dbus
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 22:51:08 +09:00
cat 608d8303ec internal/rosa/package: migrate git
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 22:37:24 +09:00
cat 1c6f30379e internal/rosa/package: migrate bzip2, curl, connman
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 22:26:40 +09:00
cat 009a4e0d58 internal/rosa/hakurei: migrate to helper
This predates the helper infrastructure, so migrate it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 22:10:39 +09:00
cat e7c8656691 internal/rosa: remove fakeroot
This is unused and broken, so remove it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 21:54:40 +09:00
cat d6be116ff8 internal/rosa/package: migrate firmware
This does not depend on the kernel.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 21:46:40 +09:00
cat 962b02cf25 internal/rosa/package: migrate kernel
This introduces bindings for extra paths and KnownChecksum, and exposes a passthrough special case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 21:40:15 +09:00
cat 6fd6d971ed internal/rosa/package: migrate mksh
This benefits greatly from the new generic helper.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 19:47:57 +09:00
cat 548c96c7ec internal/rosa/package: migrate make
This also introduces the generic helper for unusual build scripts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 19:28:18 +09:00
cat 6e8bfa6c4c internal/rosa/package: migrate cmake
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 18:42:54 +09:00
cat a770d62b9b internal/rosa/package: migrate meson
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 18:24:07 +09:00
cat ff44060763 internal/rosa/package: migrate python packages
This also migrates LLVM LIT via the newly implemented special case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 18:15:04 +09:00
cat 3010a209b5 internal/rosa/azalea: pass through source ident
For source handle special case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 18:10:11 +09:00
cat e65a3b435c internal/rosa/package/gnutls: 3.8.12 to 3.8.13
The new release came with new broken tests, but at least nettle3 can be removed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 04:36:32 +09:00
cat 23515f67c8 internal/rosa/package: migrate perl packages
Most of these are currently unused.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 04:08:22 +09:00
cat 4389df60ae internal/rosa/perl: remove obsolete helper
This method predates the helper infrastructure.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 03:40:31 +09:00
cat 8092492018 internal/rosa/perl: Makefile.PL helper
This can be invoked from azalea.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 03:24:27 +09:00
cat a7877844bf internal/rosa/package: migrate perl interpreter
Packages will be migrated separtely.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 02:49:11 +09:00
cat 1ed027846d internal/rosa/package: migrate python interpreter
Packages will take quite some work.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 02:06:17 +09:00
cat 2f376d4813 internal/rosa/package: rename buildcatrust
This causes a single rebuild due to substitution.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 01:50:03 +09:00
cat dc3810b530 internal/rosa/python: remove unnecessary input
This is added by the helper. Removing it has no effect since it is promoted by Append.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 01:48:24 +09:00
cat 6e9e8c74f3 internal/rosa: migrate buildcatrust
Other nss-related packages are unlikely to be migrated any time soon.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 01:44:23 +09:00
cat 4d60fa5632 internal/rosa: evaluate packages late
This also enables concurrent evaluation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 01:26:21 +09:00
cat 8807cbc730 internal/rosa: create metadata alongside artifact
This enables deferring evaluation of azalea-based packages and fixes the longstanding quirk of version being disjoint from other metadata.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-19 00:44:24 +09:00
cat 0e95573f18 internal/rosa/package: migrate acl
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-18 22:42:43 +09:00
cat eb2b53307a internal/rosa/package: migrate gcc
The azalea implementation used an adaptation of this as testdata.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-18 22:29:53 +09:00
cat 682b3a2ce5 internal/rosa: track evaluation time
Useful to track performance regressions over migrations.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-18 22:18:09 +09:00
cat 594221eb78 internal/rosa/package: migrate gnutls
This is the first nontrivial package to be migrated to azalea. Validated to generate identical IR.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-18 22:06:00 +09:00
cat 34822925e1 internal/rosa: migrate GNU software
These are quite trivial, so migrate them in one pass.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-18 21:23:19 +09:00
cat 37df040d85 internal/rosa: evaluate packages from fs
This migrates GNU sed to azalea, and resulting IR matches.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-18 17:54:01 +09:00
cat 0360e779f3 internal/rosa: initial azalea bindings
Supported fields are still rather minimal, but evaluation works, and resulting artifacts cure correctly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-18 02:56:38 +09:00
cat 3e236333a7 internal/rosa: panic error for invalid handle
This enables recovery and better error handling for errors originating from external azalea files.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-18 00:07:39 +09:00
cat f24ae21af1 internal/rosa/azalea: package special case
This is more efficient for the inputs array and packages in general.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-17 23:49:19 +09:00
cat 99b324fb17 cmd/mbf: update pkgserver title text
This makes more sense for its purpose.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-17 18:57:26 +09:00
kat 6f50811dc9 cmd/mbf: bring back pkgserver's favicon!
It existed in mae's #33, but ozy was not satisfied with including
a binary file identical to https://hakurei.app's favicon, and hence
removed it. However, it's possible to explicitly specify the favicon
with a link tag [1]; provided a content security policy that isn't too
strong, this should work fine.

[1]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel#icon
2026-05-17 19:47:42 +10:00
cat 6b87bac401 cmd/mbf: clone pkgserver order slices
These are no longer arrays, so must be cloned for sorting.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-17 18:30:57 +09:00
cat a967aa3b6e internal/rosa/kernel: arch-specific headers checksum
These headers differ by target architecture.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-17 18:01:11 +09:00
cat 38bc2c7508 internal/rosa: pass stage alongside state
This cleans up many function signatures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-17 17:50:30 +09:00
cat 30eb0d6a61 internal/rosa: key metadata by string
For upcoming azalea integration. The API is quite ugly right now to ease migration.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-17 15:56:53 +09:00
cat c2ff9c9fa5 internal/rosa/azalea: evaluator
Performance is sufficient for the use case, despite the fact that I could not even think of a lower-effort way to do this:

BenchmarkParse-128        	   55100	     21494 ns/op
BenchmarkEvaluate-128     	  131670	      9248 ns/op

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-17 12:44:34 +09:00
cat d38d306147 internal/rosa/azalea: ast and parser
This syntax is not final, but acts as a stopgap solution and a proof of concept.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-16 14:29:28 +09:00
cat c32c06b2e8 internal/rosa: mesa artifact
This has many dependencies.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 05:12:35 +09:00
cat 61199f734c internal/rosa/glslang: remove headers prefix
Maintainers tried to be clever with this and breaks cmake paths.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 04:57:38 +09:00
cat 87cf0d4e6b internal/rosa/mesa: libva artifact
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 04:40:04 +09:00
cat cf0dffa0f5 internal/rosa/mesa: libglvnd enable glx
Required to break circular dependency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 04:35:50 +09:00
cat 686d7ec63a internal/rosa/x: xserver artifact
Required by libglvnd test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 04:15:48 +09:00
cat 4c653b1151 internal/rosa/x: xkeyboard-config artifact
Required by xserver test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 03:59:22 +09:00
cat 0b0a63d151 internal/rosa/x: libxcb-util-wm artifact
Required by xserver xephyr.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 03:46:04 +09:00
cat 6231cfe2aa internal/rosa/x: libxcb-util-image artifact
Required by xserver xephyr.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 03:36:45 +09:00
cat 712e80890b internal/rosa/x: libxcb-util artifact
Required by xserver xephyr.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 03:25:24 +09:00
cat 3fe7d48014 internal/rosa/x: libxcb-render-util artifact
Required by xserver.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 03:09:37 +09:00
cat 16f9d39427 internal/rosa: libepoxy artifact
Required by xserver.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 02:16:55 +09:00
cat c1cd5ba07b internal/rosa: libtirpc artifact
Required by xserver.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 02:07:25 +09:00
cat 7b0cd2e472 internal/rosa/x: libXdmcp artifact
Required by xserver.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 01:44:37 +09:00
cat e580307528 internal/rosa/x: libxcvt artifact
Required by xserver.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 01:24:00 +09:00
cat ee1dffb676 internal/rosa/x: libXfont2 artifact
Required by xserver.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 01:17:27 +09:00
cat f095fcf181 internal/rosa/x: font-util and libfontenc artifact
Required by libXfont2.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 01:14:12 +09:00
cat ca8a130130 internal/rosa: freetype artifact
Required by libXfont.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-14 00:54:42 +09:00
cat 0ad6b00e41 internal/rosa/x: xkbcomp artifact
Required by xserver.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 22:24:08 +09:00
cat ad0f1cf36b internal/rosa/x: libxkbfile artifact
Required by xkbcomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 22:22:02 +09:00
cat b12d924fa2 internal/rosa: pixman artifact
Required by xserver.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 22:07:53 +09:00
cat c31d8ae41a internal/rosa/x: libXfixes artifact
Required by libva.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 21:36:47 +09:00
cat 6dbbf15c0e internal/rosa: lm_sensors artifact
Generally useful, and an optional dependency of mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 20:11:37 +09:00
cat be7de68a42 internal/rosa/perl: Test::Cmd artifact
Required by lm_sensors test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 20:05:43 +09:00
cat a759cf3666 internal/pkg: check exec substitution
This relies on the testtool having ident as relevant input to assert successful substitution.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 19:43:53 +09:00
cat 8c2dd3e984 internal/pkg: verify status kind
While it is still impossible to reliably determine the expected contents of these status files, this checks their nature for expected substitution behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 19:27:58 +09:00
cat 67038d5af4 internal/pkg: log fault in tests when available
This would otherwise only be available in verbose output, interleaved with everything else.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 18:58:18 +09:00
cat 53d8d12e7f internal/rosa/git: disable flaky test
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 18:51:11 +09:00
cat 7997d79e56 cmd/mbf: display and destroy fault entries
This change extends cmd/mbf commands for working with fault entries.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 19:06:09 +09:00
cat f2f1726190 internal/pkg: record cure faults
These are useful for troubleshooting. This change records them in a separate directory.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 17:58:18 +09:00
cat f63203cb0a internal/pkg: populate substitute status
These are not created when taking the fast path, but should be inherited from the alternative.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 16:16:37 +09:00
cat 19555c7670 internal/rosa/gtk: glib 2.88.0 to 2.88.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 00:48:38 +09:00
cat a3beab8959 internal/rosa/libucontext: 1.5 to 1.5.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 00:48:15 +09:00
cat 2ea786d6a9 internal/rosa/libbsd: libmd 1.1.0 to 1.2.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 00:47:57 +09:00
cat 747d4ec4b0 internal/rosa/libexpat: 2.8.0 to 2.8.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 00:47:32 +09:00
cat b76e6f6519 internal/rosa/tamago: 1.26.2 to 1.26.3
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 00:47:05 +09:00
cat 840d8f68bf internal/rosa/git: disable flaky test
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 00:38:59 +09:00
cat 4bede7ecdd internal/pkg: discontinue DCE resolution on signal
This serves as a stopgap measure to skip long-running DCE resolutions.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 00:29:01 +09:00
cat 487a03b5a3 internal/pkg: deduplicate DCE by ident
This eliminates edge cases where target artifacts do not compare equal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-13 00:18:27 +09:00
cat 8f3c22896a internal/pkg: DCE benchmark unwrap only
This eliminates noise at lower depths.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 19:56:59 +09:00
cat a167c1aba5 internal/pkg: hold artifact in DCE
This is significantly slower but enables much better error reporting.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 19:45:25 +09:00
cat a6008ef68b internal/pkg: benchmark early DCE
This error has never had decent performance, now is a good time to improve that.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 18:59:25 +09:00
cat 5228b27362 internal/rosa/glslang: 16.2.0 to 16.3.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 17:53:35 +09:00
cat f00d3a07ad internal/rosa/python: trove-classifiers 2026.4.28.13 to 2026.5.7.17
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 17:53:17 +09:00
cat f9538bc21b internal/rosa/python: 3.14.4 to 3.14.5
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 17:52:53 +09:00
cat 6ae5efec56 internal/rosa/gnu: gcc 15.2.0 to 16.1.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 17:52:31 +09:00
cat 14f4c59c8c internal/rosa/llvm: 22.1.4 to 22.1.5
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 17:52:13 +09:00
cat 688d43417b internal/pkg: rename measured exec type
This type is no longer exclusive to KindExecNet.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 15:23:33 +09:00
cat 9f8fafa39b internal/rosa: measure kernel headers
This makes version bumps robust and much less tedious.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 15:19:57 +09:00
cat 6643cfbeee internal/pkg: optionally measure exec artifact
Useful for verifying deterministic output without enabling network access.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 15:11:17 +09:00
cat dcde38f2e9 internal/rosa/llvm: set exclusive bit
This was missed when improving bootstrap.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 15:08:09 +09:00
cat deebbf6b1a internal/rosa/git: disable more flaky tests
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 04:13:02 +09:00
cat 0c557798bc internal/rosa/curl: 8.19.0 to 8.20.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 04:12:40 +09:00
cat 327e6ed5a2 internal/rosa/kernel: 6.12.84 to 6.12.87
This change also pins header version constants to the same values, to be updated manually on a real API change. This eliminates rebuilds on bumping kernel version.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 04:05:30 +09:00
cat 76c7a423a9 internal/rosa/git: disable more flaky tests
Again, causing too many spurious failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 03:18:12 +09:00
cat 6e113b8836 internal/pkg: content-based dependency substitution
This change introduces a new fast path for FloodArtifact. It is taken when a curing artifact has identical-by-content controlled relevant inputs and are otherwise identical to an already-cured artifact.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-12 00:19:42 +09:00
cat ce9f4b5f71 internal/rosa: vim artifact
Very useful for troubleshooting.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-10 21:45:56 +09:00
cat 8f727273ef internal/pkg: add riscv64 sums
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-10 17:12:30 +09:00
cat d0a63b942e internal/pkg: add arm64 sums
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-10 16:42:42 +09:00
cat 7f2126df32 internal/rosa/hakurei: 0.4.1 to 0.4.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-10 16:30:12 +09:00
cat 0cf0e18e35 release: 0.4.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-10 16:16:59 +09:00
cat ee5c0dd135 cmd/dist: optionally skip tests
Works around incomplete syscall translation by qemu.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-10 04:15:07 +09:00
cat 92c48d82e2 internal/rosa/go: respect check flag
These tests are also quite expensive, so optionally skip them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-10 04:01:06 +09:00
cat c79a4fe7f8 internal/rosa/stage0: add riscv64 tarball
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-09 10:51:19 +09:00
cat 0aeb2bccfb internal/rosa: libconfig artifact
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-09 00:33:27 +09:00
cat 50e079b99f internal/rosa: xcb-util-keysyms artifact
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-09 00:16:06 +09:00
cat fb2cb5005a internal/rosa: libdisplay-info artifact
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-09 00:07:43 +09:00
cat 6e73c28a92 internal/rosa: hwdata artifact
Required by libdisplay-info.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-09 00:05:40 +09:00
cat 2c08aa3674 internal/rosa/glslang: disable broken arm64 tests
These just fail on arm64, so disable them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-08 23:56:19 +09:00
cat 1af73ae7b4 internal/rosa/go: 1.26.2 to 1.26.3
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-08 23:25:57 +09:00
cat c9aa5e04b1 internal/rosa/go: bootstrap 1.25.9 to 1.25.10
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-08 23:20:39 +09:00
cat 70a38bd3b0 internal/rosa: libarchive artifact
Required by mesa, also a cleaner implementation than GNU.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-08 23:16:33 +09:00
cat 533b15da89 internal/rosa/mksh: respect check flag
This skips the test suite when OptSkipCheck is set.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-08 21:20:20 +09:00
cat a890e1d0e5 cmd/mbf: optionally override non-native flags
This is a clean workaround for configuration differences to save time during development.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-08 13:45:36 +09:00
cat e3520835bb cmd/mbf: optionally register all targets
This enables non-native cures from the daemon.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-08 13:29:58 +09:00
cat 0e56847754 cmd/mbf: add arm64 magic
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-08 00:23:09 +09:00
cat 145d03b366 cmd/mbf: optional emulated target architecture
This enables transparent cross-compilation without breaking purity.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-07 20:29:31 +09:00
cat 2886228d40 internal/rosa/qemu: build static binaries
Dynamic linking here barely saves space, and this is required for binfmt.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-07 20:25:13 +09:00
cat e1e499b79e internal/rosa/git: disable more broken tests
These are causing many spurious failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-07 20:06:11 +09:00
cat 65b7dd8b37 internal/rosa: configurable architecture
This enables curing via binfmt.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-07 20:01:44 +09:00
cat 8d72b9e5bd internal/pkg: optionally register binfmt
This transparently supports curing foreign exec artifacts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-07 19:43:06 +09:00
cat 8a3c3d145a internal/pkg: correctly generate cure expects
This needs to dereference the identifier symlink.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-07 15:57:45 +09:00
cat 575ef307ad container: binfmt registration
This arranges for binfmt entries to be registered for the container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-07 15:55:19 +09:00
cat d4144fcf7f container: optionally map uid/gid 0 as init
Unfortunately required to work around flawed APIs like binfmt_misc.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-07 15:15:47 +09:00
cat bad66facbc container: improve capability handling
This cleans up preserving caps for expansion and correctly sets privileged caps.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-07 14:27:28 +09:00
cat 4aba014eac container: abandon response on termination
This prevents blocking on early failure.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-07 00:58:02 +09:00
cat 779ba994ce container: check capability in test helper
This makes corresponding nixos tests redundant.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-06 21:05:54 +09:00
cat 917be2de93 internal/pkg/exec: close early failure before wait
This avoids a deadlock on an early container failure.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-06 18:38:16 +09:00
cat 9aad98d409 internal/rosa: suppress init verbosity in tests
This is generally the preferred option.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-06 06:54:20 +09:00
cat b0d06b67dc internal/pkg: centralise exec testdata checksums
This significantly reduces maintenance burden.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-06 06:37:58 +09:00
cat 089100f29d internal/rosa/stage0: add arm64 tarball
This was bootstrapped from the old tarball, but with the new patchset.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-06 05:47:14 +09:00
cat dfd26abf6c internal/pkg: improve output measuring
This significantly improves readability and maintainability.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-06 05:44:04 +09:00
cat 617ee21647 container/init: mount intermediate before early
This is usable as scratch space during early.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-06 00:55:45 +09:00
cat 15cdb37ec2 cmd/mbf: optional init verbosity
This output is generally not needed and only useful when debugging container machinery itself.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 23:56:16 +09:00
cat 1f0bdc7aca internal/rosa/meson: disable fallback
For some reason nodownload still allows fallback in some cases.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 21:32:19 +09:00
cat e3ffe85670 internal/rosa/python: pycparser artifact
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 20:37:09 +09:00
cat f4403ba5cd internal/rosa: libpng artifact
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 20:23:50 +09:00
cat 5a26895a22 internal/pkg: optionally suppress init verbosity
This flag applies to every exec artifact cured by the cache. It has no effect on cure outcome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 20:03:06 +09:00
cat 09d9f766a9 container: optionally suppress init verbosity
This change also removes verbose output no longer considered useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 19:59:44 +09:00
cat 6558169666 internal/rosa/x: libXrandr artifact
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 19:39:19 +09:00
cat cccf970c57 internal/rosa/x: libXrender artifact
Required by libXrandr.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 19:37:11 +09:00
cat 57ffb21690 internal/rosa/x: libXxf86vm artifact
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 19:27:59 +09:00
cat 9c560b455a internal/rosa/stage0: replace amd64 tarball
This toolchain is built with the new patchset.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 04:39:53 +09:00
cat 4c7c0fbfc6 internal/rosa/llvm: update configuration for early runtimes
These were never updated when the bootstrap was moved to stage0-only.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 04:38:17 +09:00
cat 18b3b7904e internal/rosa/llvm: exclude benchmarks
These are being built despite LLVM_BUILD_BENCHMARKS defaulting to off.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 03:11:26 +09:00
cat fefefdf734 internal/rosa/llvm: insert Rosa OS paths via musl ldso
This is cleaner than unconditionally adding rpath, and avoids breaking rpath priority.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-05 02:44:26 +09:00
cat b84bb09a80 internal/rosa/hakurei: 0.4.0 to 0.4.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-04 05:28:14 +09:00
cat 337bf20f50 release: 0.4.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-04 05:04:00 +09:00
cat 1cb792cf6e cmd/dist: increase gzip level
Performance does not matter in this case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-04 04:04:18 +09:00
cat b2b40b07e8 cmd/dist: optional verbosity
This makes output less noisy. The build is fast enough not to require progress indication.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-04 04:02:02 +09:00
cat da11b26ec1 container/initoverlay: configure via fsconfig
This works around the page size limit at the cost of negligible performance regressions.

Closes #34.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-04 02:29:56 +09:00
cat 024489e800 ext: wrap file-descriptor-based mount facilities
This only implements what is required by package container for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-04 01:54:35 +09:00
cat 0f795712b0 internal/rosa/llvm: enable LLVM_BUILD_TESTS
This arranges for tests to be built early, and is more efficient towards the end of the build.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-03 20:05:30 +09:00
cat 7e2210ff71 internal/rosa/llvm: provide runtimes early in stage0
The LLVM build system fails to handle a dynamically linked toolchain correctly, and leaks the system installation during builds.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-03 19:48:49 +09:00
cat a71a008f3c cmd/mbf: optionally build on early stages
This makes debugging the bootstrap process much less cumbersome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-03 18:46:47 +09:00
cat 162265b47e container: reject strings larger than a page
The vfs stores these values in a page obtained via GFP, and silently stops copying once the page is filled. This check prevents confusing behaviour in such cases.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-03 17:30:25 +09:00
cat 3fa7ac04e4 internal/rosa/x: combine with xcb
Separating them no longer makes sense.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-03 04:38:00 +09:00
cat bf2867d653 internal/rosa/x: libxshmfence artifact
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-03 04:35:39 +09:00
cat ec0f0f6507 internal/rosa/x: libXext artifact
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-03 04:23:20 +09:00
cat a77a802955 internal/rosa/x: xlib artifact
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-03 04:15:21 +09:00
cat 4407e14dfc internal/rosa/x: migrate to xorgproto
This is much cleaner than the many protocol packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-03 04:09:36 +09:00
cat e024d3184a internal/rosa/clang: install cpp symlink
Required by some buggy autotools scripts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-03 00:41:23 +09:00
cat 8e1bf00c2d internal/rosa/stage0: add arm64 tarball
This replaces the previous, much larger stage0 distribution.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 23:53:08 +09:00
cat b111e22050 internal/rosa/x: libxtrans artifact
Required by many X libraries.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 23:42:00 +09:00
cat 1fa458c0be internal/rosa/glslang: SPIRV-LLVM-Translator artifact
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 22:47:51 +09:00
cat 2c7ae67a67 internal/rosa/llvm: LIT args helper
This is useful for other projects using LIT.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 22:17:57 +09:00
cat 3826621b21 internal/rosa/python: lit artifact
Used by LLVM-related projects.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 22:15:37 +09:00
cat 041b505c2e internal/rosa/cmake: implicit CMAKE_BUILD_TYPE
Lack of this behaviour is a holdover from when the helper was first split from the (now removed) LLVM helper.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 21:53:38 +09:00
cat e6debce649 internal/rosa/llvm: make source independently available
This is unfortunately still required, due to the monorepo nature of LLVM.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 21:47:01 +09:00
cat aa26b86fce internal/rosa/llvm: skip multiple-compile-threads-basic on arm64
This intermittently crashes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 12:39:46 +09:00
cat a57a8fd5d8 internal/rosa/llvm: skip unwind_leaffunction on arm64
This unexpectedly passes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 05:53:00 +09:00
maemachinebroke 1d5d063d6a cmd/mbf: package status dashboard
This displays package metadata with optional status from a report.
2026-05-02 05:05:56 +09:00
cat e61628a34e cmd/mbf: test cure all via daemon
This is the daemon equivalent of CureAll in internal/rosa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 02:39:12 +09:00
cat 5a18f14929 internal/rosa/gnu: bison disable broken test
This is miscompiled by the current toolchain.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 02:23:51 +09:00
cat f12880688d internal/rosa/gnu: test skip helper
The terribleness of GNU invites interesting helpers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 05:19:54 +09:00
cat bb5bbfe16a internal/rosa/go: disable tsan test
The newly enabled tsan does not play well with go126, so disable for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-02 00:12:41 +09:00
cat 427e1ca37c internal/rosa/go: bootstrap 1.25.7 to 1.25.9
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-01 23:24:07 +09:00
cat 96fdd9ecc5 internal/rosa: disable LTO in tests
This is too expensive and not feasible for development.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-01 20:08:26 +09:00
cat 02771b655b internal/rosa/stage0: replace amd64 tarball
This is a non-LTO distribution with the new layer configuration.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-01 18:57:28 +09:00
cat d1c8d2c39b internal/rosa/gnu: skip libtool tests in stage0
This upsets the linker in stage0.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-01 05:26:40 +09:00
cat 0efd742e8a internal/rosa/llvm: enable libclc as a runtime
Enabling this as a project is deprecated.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-01 05:17:02 +09:00
cat ae1fe638d5 internal/rosa/stage0: remove unused layers
The stage0 toolchain no longer requires bundled dependencies other than the bare toolchain and environment itself.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-01 03:52:41 +09:00
cat 445d95023b internal/rosa: global preset flags
These changes preset behaviour globally. Useful for ad hoc workarounds for development or bootstrapping on resource-constrained systems.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-01 03:42:48 +09:00
cat fc66f0bb47 internal/rosa/llvm: use llvm build system
This removes the multistep bootstrap hack. Stage0 exceptions are also eliminated for a later change to bring the stage0 distribution down to just a bare toolchain, toybox and shell. This change also enables dynamic linking and ThinLTO.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-01 03:36:58 +09:00
cat 2cd6b35bee internal/rosa/cmake: run tests
This uses the standard CMake test target.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-01 03:04:59 +09:00
cat 09a216c6ec internal/rosa/perl: make /system/bin writable
This enables cure in stage0 where /system/bin is read-only.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-30 19:25:46 +09:00
cat 44d17325c2 internal/rosa: raise stage0 extra layers
This enables extras to override stage0 tarball.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-30 18:58:42 +09:00
cat 544ce77cbc internal/rosa/make: do not attempt check
This is circular during bootstrap, and tests are silently skipped without perl, so disable them explicitly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-30 17:36:46 +09:00
cat 63c3c30b23 internal/rosa/zlib: compile with -fPIC
For static linking into shared libraries. This was missed when migrating to CMake.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-30 15:55:46 +09:00
cat d23c4ecc7c internal/rosa/llvm: use correct triple for rpath
MultiarchTriple produces a generic glibc triple string.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-30 00:39:13 +09:00
cat a46656dff8 internal/rosa/python: mako 1.3.11 to 1.3.12
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-29 14:25:26 +09:00
cat 77db153ff5 internal/rosa/python: trove-classifiers 2026.1.14.14 to 2026.4.28.13
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-29 14:25:07 +09:00
cat 520d95bc07 internal/rosa/libxslt: fetch source tarball
This does not have submodules, so the overhead of git is unnecessary.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 18:31:44 +09:00
cat 451df3f4e7 internal/rosa/libxml2: fetch source tarball
This does not have submodules, so the overhead of git is unnecessary.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 18:31:28 +09:00
cat 011fac15ed internal/rosa/git: 2.53.0 to 2.54.0
This release broke httpd detection and job control on mksh.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 18:23:20 +09:00
cat 347682ad0b internal/rosa/kernel: 6.12.83 to 6.12.84
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 17:44:20 +09:00
cat 1a2b979add internal/rosa/rsync: 3.4.1 to 3.4.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 16:37:47 +09:00
cat b1c90cc380 internal/rosa/libexpat: 2.7.5 to 2.8.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 16:37:16 +09:00
cat 3a66b8143a internal/rosa/nss: 3.123 to 3.123.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 16:15:14 +09:00
cat 64bbd3aabd internal/rosa/mesa: libdrm 2.4.131 to 2.4.133
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 16:03:49 +09:00
cat 08799a13d0 internal/rosa/glslang: spirv-tools check stable versions
This hides release candidates.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 16:03:29 +09:00
cat 1aef9c3bbb internal/rosa/python: pathspec 1.0.4 to 1.1.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 16:02:19 +09:00
cat 1f38303747 internal/rosa/python: packaging 26.1 to 26.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 16:01:56 +09:00
cat 640777b00c internal/rosa/gnu: parallel 20260322 to 20260422
This pulls in bash with nonstandard hardcoded path.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 15:58:59 +09:00
cat 1d657193cf internal/rosa/kernel: disable md
This is entirely unused and is a somewhat large attack surface, so disable it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 15:48:20 +09:00
cat bab5406295 internal/rosa/go: require popcnt for x86
This backports https://go.dev/cl/746640.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-28 14:36:59 +09:00
cat 725ae7d64d nix: remove all explicit timeouts
These were useful during development because timing out is often the only indication of failure due to the terrible design of nixos vm test harness. This has become a nuisance however especially when the system is under load, so remove explicit values and fall back to the ludicrously high default.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-23 13:07:22 +09:00
cat 37a0c3967e internal/rosa/gnu: mpc fetch source tarball
This does not have submodules, so the overhead of git is unnecessary.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-23 12:57:11 +09:00
cat ea0692548f internal/rosa/gnu: coreutils 9.10 to 9.11
Test regression was fixed, dropping patch.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-23 12:30:46 +09:00
cat 48ea23e648 internal/rosa/gnu: sed 4.9 to 4.10
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-23 12:30:06 +09:00
cat 40320e4920 internal/rosa/meson: 1.11.0 to 1.11.1
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-23 12:29:17 +09:00
cat 3ca0f61632 internal/rosa/llvm: 22.1.3 to 22.1.4
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-23 12:28:55 +09:00
cat 6ffaac96e3 internal/rosa/cmake: 4.3.1 to 4.3.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-23 12:28:34 +09:00
cat 13c7713d0c internal/rosa/kernel: 6.12.82 to 6.12.83
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-23 12:28:14 +09:00
cat 42389f7ec5 internal/rosa/qemu: 10.2.2 to 11.0.0
This pulls in some python packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-23 01:15:13 +09:00
cat 30f130c691 internal/rosa/python: wheel artifact
No idea why this ended up as a package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-23 01:07:14 +09:00
cat ceb4d26087 internal/pkg: record cache variant on-disk
This makes custom artifacts much less error-prone to use.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-23 00:53:21 +09:00
cat 852f3a9b3d internal/rosa/kernel: 6.12.81 to 6.12.82
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-20 22:11:13 +09:00
cat 5e02dbdb0d internal/rosa/python: remove pypi helpers
Pypi is disallowed by policy so these helpers are no longer useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-20 02:37:10 +09:00
cat 6a3248d472 internal/rosa/python: install pyyaml from source
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-20 02:35:30 +09:00
cat 67404c98d9 internal/rosa/nss: install buildcatrust from source
Dependencies are now available, so this no longer has to rely on the release.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-20 02:09:24 +09:00
cat b9bf69cfce internal/rosa/python: install mako from source
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-20 01:55:23 +09:00
cat 4648f98272 internal/rosa/python: run tests via helper
Despite the lack of standards, pytest seems somewhat widely agreed upon.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-20 01:50:57 +09:00
cat 11d99439ac internal/rosa/python: install markupsafe from source
Required by mesa.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-20 01:26:11 +09:00
cat 39e4c5b8ac internal/rosa/python: optionally install before check
Some test suites require package to be installed globally.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-20 01:25:43 +09:00
cat e8f6db38b6 internal/rosa/python: install pytest from source
Used by many python packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 23:17:38 +09:00
cat 20d5b71575 internal/rosa/python: install iniconfig from source
This also required the setuptools-scm hack.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 22:53:32 +09:00
cat e903e7f542 internal/rosa/python: install pygments from source
This finally has its dependencies.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 22:40:43 +09:00
cat 1caa051f4d internal/rosa/python: hatchling artifact
Required by many python packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 22:35:18 +09:00
cat dcdc6f7f6d internal/rosa/python: trove-classifiers artifact
Required by hatchling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 22:32:12 +09:00
cat 5ad6f26b46 internal/rosa/python: install packaging from source
This is required by many packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 22:12:49 +09:00
cat 7ba75a79f4 internal/rosa/python: install pluggy from source
This finally has all its dependencies at this point.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 21:55:55 +09:00
cat 9ef84d3904 internal/rosa/python: setuptools-scm artifact
Awful hack required by many packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 21:38:44 +09:00
cat 3b7b6e51fb internal/rosa/python: pass build dependencies separately
This is cleaner with less duplicate code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 20:26:41 +09:00
cat b1b4debb82 internal/rosa/python: pathspec artifact
Required by hatchling, which is required by many python packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 20:13:26 +09:00
cat 021cbbc2a8 cmd/mbf: default daemon socket in cache
This location makes more sense than the current directory.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 19:50:54 +09:00
cat a4a54a4a4d cmd/mbf: remove pointless recover
This used to scrub the cache, and was not fully removed when that became nonviable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 19:49:01 +09:00
cat 04a344aac6 internal/rosa/python: flirt_core artifact
A build system required by a dependency of another build system, which is required by yet another build system.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 19:25:04 +09:00
cat 6b98156a3d internal/rosa/python: change insane strict_timestamps default
There is no scenario where this is useful, and it breaks builds.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 18:56:22 +09:00
cat 753432cf09 cmd/mbf: optionally wait for cancel
Synchronisation is not needed here during interactive use.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 18:24:11 +09:00
cat f8902e3679 internal/rosa/python: append to source path
This gets around messy projects with multiple packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 17:51:00 +09:00
cat 8ee53a5164 internal/rosa: use builtin for checksum warning
This avoids having to configure the logger early.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 17:50:12 +09:00
cat 3981d44757 internal/rosa/python: migrate setuptools to wrapper
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 15:36:43 +09:00
cat 9fd67e47b4 internal/rosa/python: wrap python package
Metadata for this is somewhat boilerplate-heavy, so wrap it to create metadata in one call.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 15:22:18 +09:00
cat 4dcec40156 cmd/mbf: close on cancel completion
Like the previous change, this enables synchronisation on the client side via epoll.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 15:03:52 +09:00
cat 9a274c78a3 cmd/mbf: close on abort completion
This enables synchronisation on the client side via epoll.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 14:53:28 +09:00
cat 5647c3a91f internal/rosa/meson: run meson test suite
Tests requiring internet access or unreasonable dependencies are removed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 01:07:20 +09:00
cat 992139c75d internal/rosa/python: extra script after install
This is generally for test suite, due to the lack of standard or widely agreed upon convention.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 00:35:24 +09:00
cat 57c69b533e internal/rosa/meson: migrate to helper
This also migrates to source from the Microsoft Github release.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 00:16:22 +09:00
cat 6f0c2a80f2 internal/rosa/python: migrate setuptools to helper
This is much cleaner, and should be functionally equivalent.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 00:04:19 +09:00
cat 08dfefb28d internal/rosa/python: pip helper
Binary pip releases are not considered acceptable, this more generic helper is required for building from source.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-19 00:03:36 +09:00
359 changed files with 22666 additions and 10032 deletions
+5 -1
View File
@@ -7,8 +7,12 @@
# go generate
/cmd/hakurei/LICENSE
/internal/pkg/testdata/testtool
/cmd/mbf/internal/pkgserver/ui/static
/internal/pkg/internal/testtool/testtool
/internal/rosa/hakurei_current.tar.gz
# cmd/dist default destination
/dist
# local packages
/internal/rosa/package/local
+1 -4
View File
@@ -1,6 +1,3 @@
#!/bin/sh -e
TOOLCHAIN_VERSION="$(go version)"
cd "$(dirname -- "$0")/"
echo "# Building cmd/dist using ${TOOLCHAIN_VERSION}."
go run -v --tags=dist ./cmd/dist
HAKUREI_DIST_MAKE='' exec "$(dirname -- "$0")/cmd/dist/dist.sh"
+2 -2
View File
@@ -20,8 +20,8 @@ func (e AbsoluteError) Error() string {
}
func (e AbsoluteError) Is(target error) bool {
var ce AbsoluteError
if !errors.As(target, &ce) {
ce, ok := errors.AsType[AbsoluteError](target)
if !ok {
return errors.Is(target, syscall.EINVAL)
}
return e == ce
+8
View File
@@ -4,15 +4,23 @@ import "strings"
const (
// SpecialOverlayEscape is the escape string for overlay mount options.
//
// Deprecated: This is no longer used and will be removed in 0.5.
SpecialOverlayEscape = `\`
// SpecialOverlayOption is the separator string between overlay mount options.
//
// Deprecated: This is no longer used and will be removed in 0.5.
SpecialOverlayOption = ","
// SpecialOverlayPath is the separator string between overlay paths.
//
// Deprecated: This is no longer used and will be removed in 0.5.
SpecialOverlayPath = ":"
)
// EscapeOverlayDataSegment escapes a string for formatting into the data
// argument of an overlay mount system call.
//
// Deprecated: This is no longer used and will be removed in 0.5.
func EscapeOverlayDataSegment(s string) string {
if s == "" {
return ""
+1
View File
@@ -0,0 +1 @@
v0.4.3
Vendored Executable
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh -e
TOOLCHAIN_VERSION="$(go version)"
cd "$(dirname -- "$0")/../.."
echo "Building cmd/dist using ${TOOLCHAIN_VERSION}."
FLAGS=''
if test -n "$VERBOSE"; then
FLAGS="$FLAGS -v"
fi
go run $FLAGS --tags=dist ./cmd/dist
+32 -15
View File
@@ -18,8 +18,13 @@ import (
"os/signal"
"path/filepath"
"runtime"
"strings"
)
//go:generate sh -c "git describe --tags > VERSION"
//go:embed VERSION
var version string
// getenv looks up an environment variable, and returns fallback if it is unset.
func getenv(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok {
@@ -42,14 +47,19 @@ func mustRun(ctx context.Context, name string, arg ...string) {
var comp []byte
func main() {
fmt.Println()
log.SetFlags(0)
log.SetPrefix("# ")
log.SetPrefix("")
version := getenv("HAKUREI_VERSION", "untagged")
verbose := os.Getenv("VERBOSE") != ""
runTests := os.Getenv("HAKUREI_DIST_MAKE") == ""
version = getenv("HAKUREI_VERSION", strings.TrimSpace(version))
prefix := getenv("PREFIX", "/usr")
destdir := getenv("DESTDIR", "dist")
if verbose {
log.Println()
}
if err := os.MkdirAll(destdir, 0755); err != nil {
log.Fatal(err)
}
@@ -76,12 +86,17 @@ func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
log.Println("Building hakurei.")
verboseFlag := "-v"
if !verbose {
verboseFlag = "-buildvcs=false"
}
log.Printf("Building hakurei for %s/%s.", runtime.GOOS, runtime.GOARCH)
mustRun(ctx, "go", "generate", "./...")
mustRun(
ctx, "go", "build",
"-trimpath",
"-v", "-o", s,
verboseFlag, "-o", s,
"-ldflags=-s -w "+
"-buildid= -linkmode external -extldflags=-static "+
"-X hakurei.app/internal/info.buildVersion="+version+" "+
@@ -90,17 +105,19 @@ func main() {
"-X main.hakureiPath="+prefix+"/bin/hakurei",
"./...",
)
fmt.Println()
log.Println()
log.Println("Testing Hakurei.")
mustRun(
ctx, "go", "test",
"-ldflags=-buildid= -linkmode external -extldflags=-static",
"./...",
)
fmt.Println()
if runTests {
log.Println("##### Testing Hakurei.")
mustRun(
ctx, "go", "test",
"-ldflags=-buildid= -linkmode external -extldflags=-static",
"./...",
)
log.Println()
}
log.Println("Creating distribution.")
log.Println("##### Creating distribution.")
const suffix = ".tar.gz"
distName := "hakurei-" + version + "-" + runtime.GOARCH
var f *os.File
@@ -121,7 +138,7 @@ func main() {
}()
h := sha512.New()
gw := gzip.NewWriter(io.MultiWriter(f, h))
gw, _ := gzip.NewWriterLevel(io.MultiWriter(f, h), gzip.BestCompression)
tw := tar.NewWriter(gw)
mustWriteHeader := func(name string, size int64, mode os.FileMode) {
+153 -20
View File
@@ -5,17 +5,91 @@
package main
import (
"context"
"crypto/rand"
"log"
"os"
"os/signal"
"runtime"
"runtime/pprof"
"slices"
"strings"
. "syscall"
"hakurei.app/internal/kobject"
"hakurei.app/internal/report"
"hakurei.app/internal/uevent"
"hakurei.app/message"
)
var r report.Reporter
func init() {
log.SetFlags(0)
log.SetPrefix("earlyinit: ")
r.SetOutput(log.Default())
// this handles SIGQUIT to provide useful debugging information without
// terminating, and prevents the runtime from throwing on the must family
// of early error reporting functions, DO NOT REMOVE
c := make(chan os.Signal, 1)
signal.Notify(c, SIGQUIT)
go func() {
for {
<-c
if p := pprof.Lookup("goroutine"); p == nil {
log.Println("initial built-in goroutine profile does not exist")
} else if err := p.WriteTo(os.Stderr, 2); err != nil {
log.Println(err)
}
}
}()
}
// fatal calls [log.Println] with v and blocks forever. Must be called from
// main. Must not be used after error reporting is set up.
func fatal(v ...any) {
log.Println(v...)
log.Println("unable to continue, please reboot and resolve the problem manually")
select {}
}
// must calls fatal with err if it is non-nil.
func must(err error) {
if err != nil {
log.Println(err)
select {}
}
}
// mustSyscall is like must, but with an additional action name.
func mustSyscall(action string, err error) {
if err != nil {
fatal("cannot "+action+":", err)
select {}
}
}
// must1 is like must, but with an additional passed through value.
func must1[T any](v T, err error) T {
must(err)
return v
}
const (
// optionSystem specifies devpath of the system device.
optionSystem = "system"
// flagVerbose increases output verbosity.
flagVerbose = "verbose"
// flagStrict sets [report.DStrict] on r.
flagStrict = "strict"
// flagNoRecover sets [report.DNoRecover] on r.
flagNoRecover = "no_recover"
)
func main() {
runtime.LockOSThread()
log.SetFlags(0)
log.SetPrefix("earlyinit: ")
var (
option map[string]string
@@ -33,15 +107,44 @@ func main() {
}
}
if err := Mount(
{
var flag uint64
if slices.Contains(flags, flagStrict) {
flag |= report.DStrict
}
if slices.Contains(flags, flagNoRecover) {
flag |= report.DNoRecover
}
log.Printf("reporting flags %x", flag)
r.SetFlags(flag)
}
msg := message.New(log.Default())
msg.SwapVerbose(slices.Contains(flags, flagVerbose))
mustSyscall("mount devtmpfs", Mount(
"devtmpfs",
"/dev/",
"devtmpfs",
MS_NOSUID|MS_NOEXEC,
"",
); err != nil {
log.Fatalf("cannot mount devtmpfs: %v", err)
}
))
must(os.Mkdir("/dev/pts/", 0))
mustSyscall("mount devpts", Mount(
"devpts",
"/dev/pts/",
"devpts",
MS_NOSUID|MS_NOEXEC,
"mode=620,ptmxmode=666",
))
must(os.Mkdir("/dev/shm/", 0))
mustSyscall("mount shm", Mount(
"shm",
"/dev/shm/",
"tmpfs",
MS_NOSUID|MS_NODEV,
"",
))
// The kernel might be unable to set up the console. When that happens,
// printk is called with "Warning: unable to open an initial console."
@@ -98,6 +201,49 @@ func main() {
"",
))
conn := must1(uevent.Dial(-128 * 1024 * 1024))
events := make(chan *uevent.Message, 1<<10)
var uuid uevent.UUID
must1(rand.Read(uuid[:]))
ctx, cancel := context.WithCancel(context.Background())
go consume(ctx, msg, &r, conn, uuid, events)
s := kobject.New(uuid, func(o *kobject.Object, env map[string]string) {
p := make([]string, 0, len(env))
for k, v := range env {
p = append(p, k+"="+v)
}
slices.Sort(p)
log.Printf("change %s: %s", o.DevPath, strings.Join(p, ", "))
}, func(err error) {
severity := report.Inconsistent
if e, ok := err.(kobject.EventError); ok && e.Kind == kobject.EBadTarget {
severity = report.Trivial
}
r.Dispatch(
severity,
"processed inconsistent uevent",
err,
)
})
go func() {
s.Consume(ctx, events)
log.Println("closing NETLINK_KOBJECT_UEVENT socket")
cancel()
if err := conn.Close(); err != nil {
log.Fatal(err) // not reached
}
}()
must(os.Mkdir("/system", 0))
if devpath := option[optionSystem]; devpath == "" {
fatal("system must be nonempty")
} else {
log.Printf("waiting for devpath pattern %q", devpath)
mustMountSystem(ctx, s, devpath)
}
// after top level has been set up
mustSyscall("remount root", Mount(
"",
@@ -113,19 +259,6 @@ func main() {
[]byte("/system/lib/firmware"),
0,
))
go dispatchModprobe(ctx, s)
}
// mustSyscall calls [log.Fatalln] if err is non-nil.
func mustSyscall(action string, err error) {
if err != nil {
log.Fatalln("cannot "+action+":", err)
}
}
// must calls [log.Fatal] with err if it is non-nil.
func must(err error) {
if err != nil {
log.Fatal(err)
}
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"os/exec"
"strings"
"hakurei.app/internal/kobject"
"hakurei.app/internal/report"
"hakurei.app/internal/uevent"
)
// ModprobeError describes an unsuccessful modprobe invocation.
type ModprobeError struct {
ModAlias string `json:"modalias"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
ExitCode int `json:"exit_code"`
}
var _ report.RepresentableError = ModprobeError{}
func (ModprobeError) Representable() {}
func (e ModprobeError) Error() string {
return fmt.Sprintf(
"modprobe exit status %d: %s",
e.ExitCode, strings.TrimSpace(e.Stderr),
)
}
// dispatchModprobe invokes modprobe for [uevent.KOBJ_ADD] events raising new
// MODALIAS strings.
func dispatchModprobe(
ctx context.Context,
s *kobject.State,
) {
aliases := make(chan string, 1<<8)
go func() {
defer close(aliases)
s.Range(ctx, func(o *kobject.Object, act uevent.KobjectAction) bool {
if act == uevent.KOBJ_ADD && o.Driver == "" && o.ModAlias != "" {
aliases <- o.ModAlias
}
return true
})
}()
for alias := range aliases {
stdout, err := exec.Command("/system/sbin/modprobe", alias).Output()
if err == nil {
if len(stdout) > 0 {
log.Println(string(stdout))
}
continue
}
exitError, ok := errors.AsType[*exec.ExitError](err)
if !ok || exitError == nil {
r.Dispatch(report.Degraded, "invoke modprobe", err)
continue
}
r.Dispatch(report.Trivial, "load device driver", ModprobeError{
ModAlias: alias,
Stdout: string(stdout),
Stderr: string(exitError.Stderr),
ExitCode: exitError.ExitCode(),
})
}
}
+71
View File
@@ -0,0 +1,71 @@
package main
import (
"context"
"errors"
"os"
"path/filepath"
"strconv"
"syscall"
"time"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/internal/kobject"
"hakurei.app/internal/uevent"
)
// mustMountSystem waits for and mounts a system device matching pattern.
func mustMountSystem(
ctx context.Context,
s *kobject.State,
pattern string,
) {
c, stop := context.WithTimeout(ctx, 30*time.Second)
defer stop()
for {
var matchErr error
var systemPath *check.Absolute
s.Range(c, func(o *kobject.Object, act uevent.KobjectAction) bool {
if (act != uevent.KOBJ_ADD && act != uevent.KOBJ_CHANGE) ||
o.Subsystem != "block" ||
o.Env["DEVTYPE"] != "disk" {
return true
}
if ok, err := filepath.Match(pattern, o.DevPath); err != nil {
matchErr = err
return false
} else if !ok {
return true
}
name, ok := o.Env["DEVNAME"]
if !ok {
return true
}
systemPath = fhs.AbsDev.Append(name)
return false
})
if c.Err() != nil {
fatal("devpath", strconv.Quote(pattern), "never appeared")
}
if matchErr != nil {
fatal("cannot match system devpath:", matchErr)
}
err := syscall.Mount(
systemPath.String(),
"/system/",
"squashfs",
0,
"threads=multi",
)
if err == nil {
break
}
if !errors.Is(err, os.ErrNotExist) {
fatal("cannot mount system:", err)
}
}
}
+104
View File
@@ -0,0 +1,104 @@
package main
import (
"context"
"time"
"hakurei.app/fhs"
"hakurei.app/internal/report"
"hakurei.app/internal/uevent"
"hakurei.app/message"
)
// newRejectColdboot returns a function to be called on every subsequent pending
// coldboot, and returns whether coldboot should proceed. Rejection is sticky.
func newRejectColdboot() func() bool {
// one coldboot per five minutes, two consecutive coldboot
const (
coldbootInterval = 5 * time.Minute
coldbootBurst = 2
)
done := make(chan struct{})
s := make(chan struct{}, coldbootBurst)
s <- struct{}{} // for early fault before reporting is ready
go func() {
t := time.NewTicker(coldbootInterval)
for {
select {
case <-done:
return
case <-t.C:
select {
case s <- struct{}{}:
default:
}
}
}
}()
return func() bool {
select {
case <-s:
return true
case <-done:
return false
default:
close(done)
return false
}
}
}
// consume continuously consumes events from conn with retries.
func consume(
ctx context.Context,
msg message.Msg,
r *report.Reporter,
conn *uevent.Conn,
uuid uevent.UUID,
events chan<- *uevent.Message,
) {
defer close(events)
nextColdboot := newRejectColdboot()
coldboot := true
retry:
if dispatchErr := conn.Consume(ctx, fhs.Sys, &uuid, events, coldboot, func(path string) {
msg.Verbose("coldboot visited", path)
}, func(err error) bool {
if _, ok := err.(uevent.NeedsColdboot); ok && !nextColdboot() {
r.Dispatch(
report.Degraded,
"rejecting coldboot loop",
err,
)
return false
}
r.Dispatch(
report.Inconsistent,
"consumed invalid message",
err,
)
return true
}, nil); dispatchErr != nil {
if _, ok := dispatchErr.(uevent.Recoverable); !ok {
r.Dispatch(
report.Fatal,
"discontinuing uevent processing due to nonrecoverable error",
dispatchErr,
)
return
}
if _, ok := dispatchErr.(uevent.NeedsColdboot); ok {
// coldboot loop rejected by handler
coldboot = false
}
goto retry
}
}
+35
View File
@@ -0,0 +1,35 @@
package main
import (
"testing"
"testing/synctest"
"time"
)
func TestRejectColdboot(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
nextColdboot := newRejectColdboot()
want := func(want bool) {
if got := nextColdboot(); got != want {
t.Fatalf("nextColdboot: %v, want %v", got, want)
}
}
synctest.Wait()
want(true)
time.Sleep(time.Hour)
synctest.Wait()
want(true)
want(true)
time.Sleep(5 * time.Minute)
synctest.Wait()
want(true)
want(false)
time.Sleep(time.Hour)
synctest.Wait()
want(false)
want(false)
})
}
+6 -5
View File
@@ -7,7 +7,8 @@ import (
"strconv"
)
// decodeJSON decodes json from r and stores it in v. A non-nil error results in a call to fatal.
// decodeJSON decodes json from r and stores it in v. A non-nil error results in
// a call to fatal.
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any) {
err := json.NewDecoder(r).Decode(v)
if err == nil {
@@ -47,14 +48,14 @@ func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any) {
}
if err := encoder.Encode(v); err != nil {
var marshalerError *json.MarshalerError
if errors.As(err, &marshalerError) && marshalerError != nil {
if e, ok := errors.AsType[*json.MarshalerError](err); ok && e != nil {
// this likely indicates an implementation error in hst
fatal("cannot encode json for " + marshalerError.Type.String() + ": " + marshalerError.Err.Error())
fatal("cannot encode json for " + e.Type.String() + ": " + e.Err.Error())
return
}
// UnsupportedTypeError, UnsupportedValueError: incorrect usage, does not need to be handled
// UnsupportedTypeError, UnsupportedValueError: incorrect usage, does
// not need to be handled
fatal("cannot write json: " + err.Error())
}
}
+1 -23
View File
@@ -21,15 +21,6 @@
// following paragraphs are considered an internal detail and not covered by the
// compatibility promise.
//
// After checking credentials, hsu checks via /proc/ the absolute pathname of
// its parent process, and fails if it does not match the hakurei pathname set
// at link time. This is not a security feature: the priv-side is considered
// trusted, and this feature makes no attempt to address the racy nature of
// querying /proc/, or debuggers attached to the parent process. Instead, this
// aims to discourage misuse and reduce confusion if the user accidentally
// stumbles upon this program. It also prevents accidental use of the incorrect
// installation of hsu in some environments.
//
// Since target container environment variables are set up in shim via the
// [container] infrastructure, the environment is used for parameters from the
// parent process.
@@ -62,7 +53,6 @@ import (
"runtime"
"slices"
"strconv"
"strings"
"syscall"
)
@@ -107,18 +97,6 @@ func main() {
return
}
var toolPath string
pexe := filepath.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") {
log.Fatal("hakurei executable has been deleted")
} else if p != hakureiPath {
log.Fatal("this program must be started by hakurei")
} else {
toolPath = p
}
// refuse to run if hsurc is not protected correctly
if s, err := os.Stat(hsuConfPath); err != nil {
log.Fatal(err)
@@ -205,7 +183,7 @@ func main() {
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
}
if err := syscall.Exec(toolPath, []string{
if err := syscall.Exec(hakureiPath, []string{
"hakurei",
"shim",
}, []string{
+48 -6
View File
@@ -2,12 +2,14 @@ package main
import (
"context"
"net/http"
"os"
"path/filepath"
"testing"
"hakurei.app/check"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
"hakurei.app/message"
)
@@ -19,10 +21,17 @@ type cache struct {
// Should generally not be used directly.
c *pkg.Cache
cures, jobs int
hostAbstract, idle bool
cures, jobs int
// Primarily to work around missing landlock LSM.
hostAbstract bool
// Set SCHED_IDLE.
idle bool
// Unset [pkg.CSuppressInit].
verboseInit bool
// Loaded artifact of [rosa.QEMU].
qemu pkg.Artifact
base string
base, mirror string
}
// open opens the underlying [pkg.Cache].
@@ -31,9 +40,6 @@ func (cache *cache) open() (err error) {
return os.ErrInvalid
}
if cache.base == "" {
cache.base = "cache"
}
var base *check.Absolute
if cache.base, err = filepath.Abs(cache.base); err != nil {
return
@@ -48,6 +54,9 @@ func (cache *cache) open() (err error) {
if cache.hostAbstract {
flags |= pkg.CHostAbstract
}
if !cache.verboseInit {
flags |= pkg.CSuppressInit
}
done := make(chan struct{})
defer close(done)
@@ -73,6 +82,39 @@ func (cache *cache) open() (err error) {
cache.jobs,
base,
)
if err != nil {
return
}
done <- struct{}{}
if cache.mirror != "" {
var pub []byte
pub, err = os.ReadFile(base.Append("ed25519.pub").String())
if err != nil {
cache.c.Close()
return
}
var r rosa.Remote
if r, err = rosa.NewRemote(cache.mirror, pub, http.DefaultClient); err != nil {
cache.c.Close()
return err
}
cache.c.SetExternal(r)
}
if cache.qemu != nil {
var pathname *check.Absolute
pathname, _, err = cache.c.Cure(cache.qemu)
if err != nil {
cache.c.Close()
return
}
for arch, entry := range rosa.Arches(pathname) {
pkg.RegisterArch(arch, entry)
}
}
return
}
+17 -6
View File
@@ -99,10 +99,9 @@ func cancelIdent(
var ident pkg.ID
if _, err := io.ReadFull(conn, ident[:]); err != nil {
return nil, false, errors.Join(err, conn.Close())
} else if err = conn.Close(); err != nil {
return nil, false, err
}
return &ident, cache.Cancel(unique.Make(ident)), nil
ok := cache.Cancel(unique.Make(ident))
return &ident, ok, conn.Close()
}
// serve services connections from a [net.UnixListener].
@@ -194,11 +193,11 @@ func serve(
}
case specialAbort:
log.Println("aborting all pending cures")
cm.c.Abort()
if _err := conn.Close(); _err != nil {
log.Println(_err)
}
log.Println("aborting all pending cures")
cm.c.Abort()
}
return
@@ -306,6 +305,7 @@ func cancelRemote(
ctx context.Context,
addr *net.UnixAddr,
a pkg.Artifact,
wait bool,
) error {
done, conn, err := dial(ctx, addr)
if err != nil {
@@ -324,13 +324,19 @@ func cancelRemote(
} else if n != len(id) {
return errors.Join(io.ErrShortWrite, conn.Close())
}
return conn.Close()
if wait {
if _, err = conn.Read(make([]byte, 1)); err == io.EOF {
err = nil
}
}
return errors.Join(err, conn.Close())
}
// abortRemote aborts all [pkg.Artifact] curing on a daemon.
func abortRemote(
ctx context.Context,
addr *net.UnixAddr,
wait bool,
) error {
done, conn, err := dial(ctx, addr)
if err != nil {
@@ -339,5 +345,10 @@ func abortRemote(
defer close(done)
err = writeSpecialHeader(conn, specialAbort)
if wait && err == nil {
if _, err = conn.Read(make([]byte, 1)); err == io.EOF {
err = nil
}
}
return errors.Join(err, conn.Close())
}
+2 -2
View File
@@ -106,11 +106,11 @@ func TestDaemon(t *testing.T) {
}
}()
if err = cancelRemote(ctx, &addr, pkg.NewFile("nonexistent", nil)); err != nil {
if err = cancelRemote(ctx, &addr, pkg.NewFile("nonexistent", nil), true); err != nil {
t.Fatalf("cancelRemote: error = %v", err)
}
if err = abortRemote(ctx, &addr); err != nil {
if err = abortRemote(ctx, &addr, true); err != nil {
t.Fatalf("abortRemote: error = %v", err)
}
+14 -23
View File
@@ -6,6 +6,7 @@ import (
"io"
"os"
"strings"
"unique"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
@@ -17,25 +18,12 @@ func commandInfo(
args []string,
w io.Writer,
writeStatus bool,
reportPath string,
r *rosa.Report,
) (err error) {
if len(args) == 0 {
return errors.New("info requires at least 1 argument")
}
var r *rosa.Report
if reportPath != "" {
if r, err = rosa.OpenReport(reportPath); err != nil {
return err
}
defer func() {
if closeErr := r.Close(); err == nil {
err = closeErr
}
}()
defer r.HandleAccess(&err)()
}
// recovered by HandleAccess
mustPrintln := func(a ...any) {
if _, _err := fmt.Fprintln(w, a...); _err != nil {
@@ -48,17 +36,19 @@ func commandInfo(
}
}
t := rosa.Native().Std()
for i, name := range args {
if p, ok := rosa.ResolveName(name); !ok {
handle := rosa.ArtifactH(unique.Make(name))
if meta, a := t.Load(handle); meta == nil {
return fmt.Errorf("unknown artifact %q", name)
} else {
var suffix string
if version := rosa.Std.Version(p); version != rosa.Unversioned {
suffix += "-" + version
if meta.Version != rosa.Unversioned {
suffix += "-" + meta.Version
}
mustPrintln("name : " + name + suffix)
meta := rosa.GetMetadata(p)
mustPrintln("description : " + meta.Description)
if meta.Website != "" {
mustPrintln("website : " +
@@ -67,9 +57,10 @@ func commandInfo(
if len(meta.Dependencies) > 0 {
mustPrint("depends on :")
for _, d := range meta.Dependencies {
s := rosa.GetMetadata(d).Name
if version := rosa.Std.Version(d); version != rosa.Unversioned {
s += "-" + version
_meta, _ := rosa.Native().Std().MustLoad(d)
s := _meta.Name
if _meta.Version != rosa.Unversioned {
s += "-" + _meta.Version
}
mustPrint(" " + s)
}
@@ -81,7 +72,7 @@ func commandInfo(
if r == nil {
var f io.ReadSeekCloser
err = cm.Do(func(cache *pkg.Cache) (err error) {
f, err = cache.OpenStatus(rosa.Std.Load(p))
f, err = cache.OpenStatus(a)
return
})
if err != nil {
@@ -100,7 +91,7 @@ func commandInfo(
}
}
} else if err = cm.Do(func(cache *pkg.Cache) (err error) {
status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p)))
status, n := r.ArtifactOf(cache.Ident(a))
if status == nil {
mustPrintln(
statusPrefix + "not in report",
+39 -19
View File
@@ -10,6 +10,7 @@ import (
"strings"
"syscall"
"testing"
"unique"
"unsafe"
"hakurei.app/internal/pkg"
@@ -20,6 +21,14 @@ import (
func TestInfo(t *testing.T) {
t.Parallel()
_t := rosa.Native().Std()
qemuMeta, _ := _t.Load(rosa.H("qemu"))
glibMeta, _ := _t.Load(rosa.H("glib"))
zlibMeta, zlib := _t.Load(rosa.H("zlib"))
zstdMeta, _ := _t.Load(rosa.H("zstd"))
hakureiMeta, _ := _t.Load(rosa.H("hakurei"))
hakureiDistMeta, _ := _t.Load(rosa.H("hakurei-dist"))
testCases := []struct {
name string
args []string
@@ -29,24 +38,24 @@ func TestInfo(t *testing.T) {
wantErr any
}{
{"qemu", []string{"qemu"}, nil, "", `
name : qemu-` + rosa.Std.Version(rosa.QEMU) + `
name : qemu-` + qemuMeta.Version + `
description : a generic and open source machine emulator and virtualizer
website : https://www.qemu.org
depends on : glib-` + rosa.Std.Version(rosa.GLib) + ` zstd-` + rosa.Std.Version(rosa.Zstd) + `
depends on : glib-` + glibMeta.Version + ` zstd-` + zstdMeta.Version + `
`, nil},
{"multi", []string{"hakurei", "hakurei-dist"}, nil, "", `
name : hakurei-` + rosa.Std.Version(rosa.Hakurei) + `
name : hakurei-` + hakureiMeta.Version + `
description : low-level userspace tooling for Rosa OS
website : https://hakurei.app
name : hakurei-dist-` + rosa.Std.Version(rosa.HakureiDist) + `
name : hakurei-dist-` + hakureiDistMeta.Version + `
description : low-level userspace tooling for Rosa OS (distribution tarball)
website : https://hakurei.app
`, nil},
{"nonexistent", []string{"zlib", "\x00"}, nil, "", `
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
name : zlib-` + zlibMeta.Version + `
description : lossless data-compression library
website : https://zlib.net
@@ -56,12 +65,12 @@ website : https://zlib.net
"zstd": "internal/pkg (amd64) on satori\n",
"hakurei": "internal/pkg (amd64) on satori\n\n",
}, "", `
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
name : zlib-` + zlibMeta.Version + `
description : lossless data-compression library
website : https://zlib.net
status : not yet cured
name : zstd-` + rosa.Std.Version(rosa.Zstd) + `
name : zstd-` + zstdMeta.Version + `
description : a fast compression algorithm
website : https://facebook.github.io/zstd
status : internal/pkg (amd64) on satori
@@ -70,19 +79,19 @@ status : internal/pkg (amd64) on satori
{"status cache perm", []string{"zlib"}, map[string]string{
"zlib": "\x00",
}, "", `
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
name : zlib-` + zlibMeta.Version + `
description : lossless data-compression library
website : https://zlib.net
`, func(cm *cache) error {
return &os.PathError{
Op: "open",
Path: filepath.Join(cm.base, "status", pkg.Encode(cm.c.Ident(rosa.Std.Load(rosa.Zlib)).Value())),
Path: filepath.Join(cm.base, "status", pkg.Encode(cm.c.Ident(zlib).Value())),
Err: syscall.EACCES,
}
}},
{"status report", []string{"zlib"}, nil, strings.Repeat("\x00", len(pkg.Checksum{})+8), `
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
name : zlib-` + zlibMeta.Version + `
description : lossless data-compression library
website : https://zlib.net
status : not in report
@@ -95,7 +104,7 @@ status : not in report
var (
cm *cache
buf strings.Builder
rp string
r *rosa.Report
)
if tc.status != nil || tc.report != "" {
@@ -108,20 +117,31 @@ status : not in report
}
if tc.report != "" {
rp = filepath.Join(t.TempDir(), "report")
if err := os.WriteFile(
rp,
pathname := filepath.Join(t.TempDir(), "report")
err := os.WriteFile(
pathname,
unsafe.Slice(unsafe.StringData(tc.report), len(tc.report)),
0400,
); err != nil {
)
if err != nil {
t.Fatal(err)
}
r, err = rosa.OpenReport(pathname)
if err != nil {
t.Fatal(err)
}
defer func() {
if err = r.Close(); err != nil {
t.Fatal(err)
}
}()
}
if tc.status != nil {
for name, status := range tc.status {
p, ok := rosa.ResolveName(name)
if !ok {
_, a := _t.Load(rosa.ArtifactH(unique.Make(name)))
if a == nil {
t.Fatalf("invalid name %q", name)
}
perm := os.FileMode(0400)
@@ -132,7 +152,7 @@ status : not in report
return os.WriteFile(filepath.Join(
cm.base,
"status",
pkg.Encode(cache.Ident(rosa.Std.Load(p)).Value()),
pkg.Encode(cache.Ident(a).Value()),
), unsafe.Slice(unsafe.StringData(status), len(status)), perm)
}); err != nil {
t.Fatalf("Do: error = %v", err)
@@ -157,7 +177,7 @@ status : not in report
tc.args,
&buf,
cm != nil,
rp,
r,
); !reflect.DeepEqual(err, wantErr) {
t.Fatalf("commandInfo: error = %v, want %v", err, wantErr)
}
+202
View File
@@ -0,0 +1,202 @@
// Package pkgserver implements the package metadata service backend.
package pkgserver
import (
"context"
"encoding/json"
"log"
"net/http"
"net/url"
"path"
"strconv"
"sync"
"time"
"hakurei.app/internal/info"
"hakurei.app/internal/rosa"
)
// for lazy initialisation of serveInfo
var (
infoPayload struct {
// Current package count.
Count int `json:"count"`
// Hakurei version, set at link time.
HakureiVersion string `json:"hakurei_version"`
}
infoPayloadOnce sync.Once
)
// handleInfo writes constant system information.
func handleInfo(w http.ResponseWriter, _ *http.Request) {
infoPayloadOnce.Do(func() {
infoPayload.Count = len(rosa.Native().Collect())
infoPayload.HakureiVersion = info.Version()
})
// TODO(mae): cache entire response if no additional fields are planned
writeAPIPayload(w, infoPayload)
}
// newStatusHandler returns a [http.HandlerFunc] that offers status files for
// viewing or download, if available.
func (index *packageIndex) newStatusHandler(disposition bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
m, ok := index.names[path.Base(r.URL.Path)]
if !ok || !m.HasReport {
http.NotFound(w, r)
return
}
contentType := "text/plain; charset=utf-8"
if disposition {
contentType = "application/octet-stream"
// quoting like this is unsound, but okay, because metadata is hardcoded
contentDisposition := `attachment; filename="`
contentDisposition += m.Name + "-"
if m.Version != "" {
contentDisposition += m.Version + "-"
}
contentDisposition += m.ids + `.log"`
w.Header().Set("Content-Disposition", contentDisposition)
}
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := func() (err error) {
defer index.handleAccess(&err)()
_, err = w.Write(m.status)
return
}(); err != nil {
log.Println(err)
http.Error(
w, "cannot deliver status, contact maintainers",
http.StatusInternalServerError,
)
}
}
}
// handleGet writes a slice of metadata with specified order.
func (index *packageIndex) handleGet(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
limit, err := strconv.Atoi(q.Get("limit"))
if err != nil || limit > 100 || limit < 1 {
http.Error(
w, "limit must be an integer between 1 and 100",
http.StatusBadRequest,
)
return
}
i, err := strconv.Atoi(q.Get("index"))
if err != nil || i >= len(index.sorts[0]) || i < 0 {
http.Error(
w, "index must be an integer between 0 and "+
strconv.Itoa(len(index.sorts[0])-1),
http.StatusBadRequest,
)
return
}
sort, err := strconv.Atoi(q.Get("sort"))
if err != nil || sort >= len(index.sorts) || sort < 0 {
http.Error(
w, "sort must be an integer between 0 and "+
strconv.Itoa(sortOrderEnd),
http.StatusBadRequest,
)
return
}
values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
writeAPIPayload(w, &struct {
Values []*metadata `json:"values"`
}{values})
}
func (index *packageIndex) handleSearch(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
limit, err := strconv.Atoi(q.Get("limit"))
if err != nil || limit > 100 || limit < 1 {
http.Error(
w, "limit must be an integer between 1 and 100",
http.StatusBadRequest,
)
return
}
i, err := strconv.Atoi(q.Get("index"))
if err != nil || i >= len(index.sorts[0]) || i < 0 {
http.Error(
w, "index must be an integer between 0 and "+
strconv.Itoa(len(index.sorts[0])-1),
http.StatusBadRequest,
)
return
}
search, err := url.QueryUnescape(q.Get("search"))
if len(search) > 100 || err != nil {
http.Error(
w, "search must be a string between 0 and 100 characters long",
http.StatusBadRequest,
)
return
}
desc := q.Get("desc") == "true"
n, res, err := index.performSearchQuery(limit, i, search, desc)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
writeAPIPayload(w, &struct {
Count int `json:"count"`
Values []searchResult `json:"values"`
}{n, res})
}
// apiVersion is the name of the current API revision, as part of the pattern.
const apiVersion = "v1"
// registerAPI registers API handler functions.
func (index *packageIndex) registerAPI(mux *http.ServeMux) {
mux.HandleFunc("GET /api/"+apiVersion+"/info", handleInfo)
mux.HandleFunc("GET /api/"+apiVersion+"/get", index.handleGet)
mux.HandleFunc("GET /api/"+apiVersion+"/search", index.handleSearch)
mux.HandleFunc("GET /api/"+apiVersion+"/status/", index.newStatusHandler(false))
mux.HandleFunc("GET /status/", index.newStatusHandler(true))
}
// Register arranges for mux to service API requests.
func Register(ctx context.Context, mux *http.ServeMux, report *rosa.Report) error {
var index packageIndex
index.search = make(searchCache)
if err := index.populate(report); err != nil {
return err
}
ticker := time.NewTicker(1 * time.Minute)
go func() {
for {
select {
case <-ctx.Done():
ticker.Stop()
return
case <-ticker.C:
index.search.clean()
}
}
}()
index.registerAPI(mux)
return nil
}
// writeAPIPayload sets headers common to API responses and encodes payload as
// JSON for the response body.
func writeAPIPayload(w http.ResponseWriter, payload any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
if err := json.NewEncoder(w).Encode(payload); err != nil {
log.Println(err)
http.Error(
w, "cannot encode payload, contact maintainers",
http.StatusInternalServerError,
)
}
}
+111
View File
@@ -0,0 +1,111 @@
package pkgserver
import (
"net/http"
"net/http/httptest"
"strconv"
"testing"
"hakurei.app/internal/info"
"hakurei.app/internal/rosa"
)
// prefix is prepended to every API path.
const prefix = "/api/" + apiVersion + "/"
func TestAPIInfo(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
handleInfo(w, httptest.NewRequestWithContext(
t.Context(),
http.MethodGet,
prefix+"info",
nil,
))
resp := w.Result()
checkStatus(t, resp, http.StatusOK)
checkAPIHeader(t, w.Header())
checkPayload(t, resp, struct {
Count int `json:"count"`
HakureiVersion string `json:"hakurei_version"`
}{len(rosa.Native().Collect()), info.Version()})
}
func TestAPIGet(t *testing.T) {
t.Parallel()
const target = prefix + "get"
index := newIndex(t)
newRequest := func(suffix string) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
index.handleGet(w, httptest.NewRequestWithContext(
t.Context(),
http.MethodGet,
target+suffix,
nil,
))
return w
}
checkValidate := func(t *testing.T, suffix string, vmin, vmax int, wantErr string) {
t.Run("invalid", func(t *testing.T) {
t.Parallel()
w := newRequest("?" + suffix + "=invalid")
resp := w.Result()
checkError(t, resp, wantErr, http.StatusBadRequest)
})
t.Run("min", func(t *testing.T) {
t.Parallel()
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmin-1))
resp := w.Result()
checkError(t, resp, wantErr, http.StatusBadRequest)
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmin))
resp = w.Result()
checkStatus(t, resp, http.StatusOK)
})
t.Run("max", func(t *testing.T) {
t.Parallel()
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmax+1))
resp := w.Result()
checkError(t, resp, wantErr, http.StatusBadRequest)
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmax))
resp = w.Result()
checkStatus(t, resp, http.StatusOK)
})
}
t.Run("limit", func(t *testing.T) {
t.Parallel()
checkValidate(
t, "index=0&sort=0&limit", 1, 100,
"limit must be an integer between 1 and 100",
)
})
count := len(rosa.Native().Collect())
t.Run("index", func(t *testing.T) {
t.Parallel()
checkValidate(
t, "limit=1&sort=0&index", 0, count-1,
"index must be an integer between 0 and "+strconv.Itoa(count-1),
)
})
t.Run("sort", func(t *testing.T) {
t.Parallel()
checkValidate(
t, "index=0&limit=1&sort", 0, int(sortOrderEnd),
"sort must be an integer between 0 and "+strconv.Itoa(int(sortOrderEnd)),
)
})
}
+108
View File
@@ -0,0 +1,108 @@
package pkgserver
import (
"cmp"
"errors"
"slices"
"strings"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
)
const (
declarationAscending = iota
declarationDescending
nameAscending
nameDescending
sizeAscending
sizeDescending
sortOrderEnd = iota - 1
)
// packageIndex refers to metadata by name and various sort orders.
type packageIndex struct {
sorts [sortOrderEnd + 1][]*metadata
names map[string]*metadata
search searchCache
// Taken from [rosa.Report] if available.
handleAccess func(*error) func()
}
// metadata holds [rosa.Metadata] extended with additional information.
type metadata struct {
handle rosa.ArtifactH
*rosa.Metadata
// Copied from [rosa.Metadata], [rosa.Unversioned] is equivalent to the zero
// value. Otherwise, the zero value is invalid.
Version string `json:"version,omitempty"`
// Output data size, available if present in report.
Size int64 `json:"size,omitempty"`
// Whether the underlying [pkg.Artifact] is present in the report.
HasReport bool `json:"report"`
// Ident string encoded ahead of time.
ids string
// Backed by [rosa.Report], access must be prepared by HandleAccess.
status []byte
}
// populate deterministically populates packageIndex, optionally with a report.
func (index *packageIndex) populate(report *rosa.Report) (err error) {
if report != nil {
defer report.HandleAccess(&err)()
index.handleAccess = report.HandleAccess
}
handles := rosa.Native().Collect()
work := make([]*metadata, len(handles))
index.names = make(map[string]*metadata)
ir := pkg.NewIR()
for i, handle := range handles {
meta, a := rosa.Native().Std().MustLoad(handle)
m := metadata{
handle: handle,
Metadata: meta,
Version: meta.Version,
}
if m.Version == "" {
return errors.New("invalid version from " + m.Name)
}
if m.Version == rosa.Unversioned {
m.Version = ""
}
if report != nil {
id := ir.Ident(a)
m.ids = pkg.Encode(id.Value())
m.status, m.Size = report.ArtifactOf(id)
m.HasReport = m.Size >= 0
}
work[i] = &m
index.names[m.Name] = &m
}
index.sorts[declarationAscending] = work
index.sorts[declarationDescending] = slices.Clone(work)
slices.Reverse(index.sorts[declarationDescending][:])
index.sorts[nameAscending] = slices.Clone(work)
slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
return strings.Compare(a.Name, b.Name)
})
index.sorts[nameDescending] = slices.Clone(index.sorts[nameAscending])
slices.Reverse(index.sorts[nameDescending][:])
index.sorts[sizeAscending] = slices.Clone(work)
slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int {
return cmp.Compare(a.Size, b.Size)
})
index.sorts[sizeDescending] = slices.Clone(index.sorts[sizeAscending])
slices.Reverse(index.sorts[sizeDescending][:])
return
}
+96
View File
@@ -0,0 +1,96 @@
package pkgserver
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"testing"
)
// newIndex returns the address of a newly populated packageIndex.
func newIndex(t *testing.T) *packageIndex {
t.Helper()
var index packageIndex
if err := index.populate(nil); err != nil {
t.Fatalf("populate: error = %v", err)
}
return &index
}
// checkStatus checks response status code.
func checkStatus(t *testing.T, resp *http.Response, want int) {
t.Helper()
if resp.StatusCode != want {
t.Errorf(
"StatusCode: %s, want %s",
http.StatusText(resp.StatusCode),
http.StatusText(want),
)
}
}
// checkHeader checks the value of a header entry.
func checkHeader(t *testing.T, h http.Header, key, want string) {
t.Helper()
if got := h.Get(key); got != want {
t.Errorf("%s: %q, want %q", key, got, want)
}
}
// checkAPIHeader checks common entries set for API endpoints.
func checkAPIHeader(t *testing.T, h http.Header) {
t.Helper()
checkHeader(t, h, "Content-Type", "application/json; charset=utf-8")
checkHeader(t, h, "Cache-Control", "no-cache, no-store, must-revalidate")
checkHeader(t, h, "Pragma", "no-cache")
checkHeader(t, h, "Expires", "0")
}
// checkPayloadFunc checks the JSON response of an API endpoint by passing it to f.
func checkPayloadFunc[T any](
t *testing.T,
resp *http.Response,
f func(got *T) bool,
) {
t.Helper()
var got T
r := io.Reader(resp.Body)
if testing.Verbose() {
var buf bytes.Buffer
r = io.TeeReader(r, &buf)
defer func() { t.Helper(); t.Log(buf.String()) }()
}
if err := json.NewDecoder(r).Decode(&got); err != nil {
t.Fatalf("Decode: error = %v", err)
}
if !f(&got) {
t.Errorf("Body: %#v", got)
}
}
// checkPayload checks the JSON response of an API endpoint.
func checkPayload[T any](t *testing.T, resp *http.Response, want T) {
t.Helper()
checkPayloadFunc(t, resp, func(got *T) bool {
return reflect.DeepEqual(got, &want)
})
}
func checkError(t *testing.T, resp *http.Response, error string, code int) {
t.Helper()
checkStatus(t, resp, code)
if got, _ := io.ReadAll(resp.Body); string(got) != fmt.Sprintln(error) {
t.Errorf("Body: %q, want %q", string(got), error)
}
}
+81
View File
@@ -0,0 +1,81 @@
package pkgserver
import (
"cmp"
"maps"
"regexp"
"slices"
"time"
)
type searchCache map[string]searchCacheEntry
type searchResult struct {
NameIndices [][]int `json:"name_matches"`
DescIndices [][]int `json:"desc_matches,omitempty"`
Score float64 `json:"score"`
*metadata
}
type searchCacheEntry struct {
query string
results []searchResult
expiry time.Time
}
func (index *packageIndex) performSearchQuery(limit int, i int, search string, desc bool) (int, []searchResult, error) {
query := search
if desc {
query += ";withDesc"
}
entry, ok := index.search[query]
if ok && len(entry.results) > 0 {
return len(entry.results), entry.results[min(i, len(entry.results)-1):min(i+limit, len(entry.results))], nil
}
regex, err := regexp.Compile(search)
if err != nil {
return 0, make([]searchResult, 0), err
}
res := make([]searchResult, 0)
for p := range maps.Values(index.names) {
nameIndices := regex.FindAllIndex([]byte(p.Name), -1)
var descIndices [][]int = nil
if desc {
descIndices = regex.FindAllIndex([]byte(p.Description), -1)
}
if nameIndices == nil && descIndices == nil {
continue
}
score := float64(indexsum(nameIndices)) / (float64(len(nameIndices)) + 1)
if desc {
score += float64(indexsum(descIndices)) / (float64(len(descIndices)) + 1) / 10.0
}
res = append(res, searchResult{
NameIndices: nameIndices,
DescIndices: descIndices,
Score: score,
metadata: p,
})
}
slices.SortFunc(res[:], func(a, b searchResult) int { return -cmp.Compare(a.Score, b.Score) })
expiry := time.Now().Add(1 * time.Minute)
entry = searchCacheEntry{
query: search,
results: res,
expiry: expiry,
}
index.search[query] = entry
return len(res), res[i:min(i+limit, len(entry.results))], nil
}
func (s *searchCache) clean() {
maps.DeleteFunc(*s, func(_ string, v searchCacheEntry) bool {
return v.expiry.Before(time.Now())
})
}
func indexsum(in [][]int) int {
sum := 0
for i := range in {
sum += in[i][1] - in[i][0]
}
return sum
}
+58
View File
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<link rel="icon" href="https://hakurei.app/favicon.ico"/>
<title>Rosa OS Packages</title>
<script src="index.js"></script>
</head>
<body>
<h1>Rosa OS Packages</h1>
<div class="top-controls" id="top-controls-regular">
<p>Showing entries <span id="entry-counter"></span>.</p>
<span id="search-bar">
<label for="search">Search: </label>
<input type="text" name="search" id="search"/>
<button onclick="doSearch()">Find</button>
<label for="include-desc">Include descriptions: </label>
<input type="checkbox" name="include-desc" id="include-desc" checked/>
</span>
<div><label for="count">Entries per page: </label><select name="count" id="count">
<option value="10">10</option>
<option value="20">20</option>
<option value="30">30</option>
<option value="50">50</option>
</select></div>
<div><label for="sort">Sort by: </label><select name="sort" id="sort">
<option value="0">Definition (ascending)</option>
<option value="1">Definition (descending)</option>
<option value="2">Name (ascending)</option>
<option value="3">Name (descending)</option>
<option value="4">Size (ascending)</option>
<option value="5">Size (descending)</option>
</select></div>
</div>
<div class="top-controls" id="search-top-controls" hidden>
<p>Showing search results <span id="search-entry-counter"></span> for query "<span id="search-query"></span>".</p>
<button onclick="exitSearch()">Back</button>
<div><label for="search-count">Entries per page: </label><select name="search-count" id="search-count">
<option value="10">10</option>
<option value="20">20</option>
<option value="30">30</option>
<option value="50">50</option>
</select></div>
<p>Sorted by best match</p>
</div>
<div class="page-controls"><a href="javascript:prevPage()">&laquo; Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next &raquo;</a></div>
<table id="pkg-list">
<tr><td>Loading...</td></tr>
</table>
<div class="page-controls"><a href="javascript:prevPage()">&laquo; Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next &raquo;</a></div>
<footer>
<p>&copy;<a href="https://hakurei.app/">Hakurei</a> (<span id="hakurei-version">unknown</span>). Licensed under the MIT license.</p>
</footer>
<script>main();</script>
</body>
</html>
+331
View File
@@ -0,0 +1,331 @@
interface PackageIndexEntry {
name: string
size?: number
description?: string
website?: string
version?: string
report?: boolean
}
function entryToHTML(entry: PackageIndexEntry | SearchResult): HTMLTableRowElement {
let v = entry.version != null ? `<span>${escapeHtml(entry.version)}</span>` : ""
let s = entry.size != null && entry.size > 0 ? `<p>Size: ${toByteSizeString(entry.size)} (${entry.size})</p>` : ""
let n: string
let d: string
if ('name_matches' in entry) {
n = `<h2>${nameMatches(entry as SearchResult)} ${v}</h2>`
} else {
n = `<h2>${escapeHtml(entry.name)} ${v}</h2>`
}
if ('desc_matches' in entry && STATE.getIncludeDescriptions()) {
d = descMatches(entry as SearchResult)
} else {
d = (entry as PackageIndexEntry).description != null ? `<p>${escapeHtml((entry as PackageIndexEntry).description)}</p>` : ""
}
let w = entry.website != null ? `<a href="${encodeURI(entry.website)}">Website</a>` : ""
let r = entry.report ? `Log (<a href=\"${encodeURI('/api/v1/status/' + entry.name)}\">View</a> | <a href=\"${encodeURI('/status/' + entry.name)}\">Download</a>)` : ""
let row = <HTMLTableRowElement>(document.createElement('tr'))
row.innerHTML = `<td>
${n}
${d}
${s}
${w}
${r}
</td>`
return row
}
function nameMatches(sr: SearchResult): string {
return markMatches(sr.name, sr.name_matches)
}
function descMatches(sr: SearchResult): string {
return markMatches(sr.description!, sr.desc_matches)
}
function markMatches(str: string, indices: [number, number][]): string {
if (indices == null) {
return str
}
let out: string = ""
let j = 0
for (let i = 0; i < str.length; i++) {
if (j < indices.length) {
if (i === indices[j][0]) {
out += `<mark>${escapeHtmlChar(str[i])}`
continue
}
if (i === indices[j][1]) {
out += `</mark>${escapeHtmlChar(str[i])}`
j++
continue
}
}
out += escapeHtmlChar(str[i])
}
if (indices[j] !== undefined) {
out += "</mark>"
}
return out
}
function toByteSizeString(bytes: number): string {
if (bytes == null) return `unspecified`
if (bytes < 1024) return `${bytes}B`
if (bytes < Math.pow(1024, 2)) return `${(bytes / 1024).toFixed(2)}kiB`
if (bytes < Math.pow(1024, 3)) return `${(bytes / Math.pow(1024, 2)).toFixed(2)}MiB`
if (bytes < Math.pow(1024, 4)) return `${(bytes / Math.pow(1024, 3)).toFixed(2)}GiB`
if (bytes < Math.pow(1024, 5)) return `${(bytes / Math.pow(1024, 4)).toFixed(2)}TiB`
return "not only is it big, it's large"
}
const API_VERSION = 1
const ENDPOINT = `/api/v${API_VERSION}`
interface InfoPayload {
count?: number
hakurei_version?: string
}
async function infoRequest(): Promise<InfoPayload> {
const res = await fetch(`${ENDPOINT}/info`)
const payload = await res.json()
return payload as InfoPayload
}
interface GetPayload {
values?: PackageIndexEntry[]
}
enum SortOrders {
DeclarationAscending,
DeclarationDescending,
NameAscending,
NameDescending
}
async function getRequest(limit: number, index: number, sort: SortOrders): Promise<GetPayload> {
const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`)
const payload = await res.json()
return payload as GetPayload
}
interface SearchResult extends PackageIndexEntry {
name_matches: [number, number][]
desc_matches: [number, number][]
score: number
}
interface SearchPayload {
count?: number
values?: SearchResult[]
}
async function searchRequest(limit: number, index: number, search: string, desc: boolean): Promise<SearchPayload> {
const res = await fetch(`${ENDPOINT}/search?limit=${limit}&index=${index}&search=${encodeURIComponent(search)}&desc=${desc}`)
if (!res.ok) {
exitSearch()
alert("invalid search query!")
return Promise.reject(res.statusText)
}
const payload = await res.json()
return payload as SearchPayload
}
class State {
entriesPerPage: number = 10
entryIndex: number = 0
maxTotal: number = 0
maxEntries: number = 0
sort: SortOrders = SortOrders.DeclarationAscending
search: boolean = false
getEntriesPerPage(): number {
return this.entriesPerPage
}
setEntriesPerPage(entriesPerPage: number) {
this.entriesPerPage = entriesPerPage
this.setEntryIndex(Math.floor(this.getEntryIndex() / entriesPerPage) * entriesPerPage)
}
getEntryIndex(): number {
return this.entryIndex
}
setEntryIndex(entryIndex: number) {
this.entryIndex = entryIndex
this.updatePage()
this.updateRange()
this.updateListings()
}
getMaxTotal(): number {
return this.maxTotal
}
setMaxTotal(max: number) {
this.maxTotal = max
}
getSortOrder(): SortOrders {
return this.sort
}
setSortOrder(sortOrder: SortOrders) {
this.sort = sortOrder
this.setEntryIndex(0)
}
updatePage() {
let page = Math.ceil(((this.getEntryIndex() + this.getEntriesPerPage()) - 1) / this.getEntriesPerPage())
for (let e of document.getElementsByClassName("page-number")) {
(e as HTMLInputElement).value = String(page)
}
}
updateRange() {
let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxTotal())
document.getElementById("entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxTotal()}`
if (this.search) {
document.getElementById("search-entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.maxTotal}/${this.maxEntries}`
document.getElementById("search-query")!.innerHTML = `<code>${escapeHtml(this.getSearchQuery())}</code>`
}
}
getSearchQuery(): string {
let queryString = document.getElementById("search")!;
return (queryString as HTMLInputElement).value
}
getIncludeDescriptions(): boolean {
let includeDesc = document.getElementById("include-desc")!;
return (includeDesc as HTMLInputElement).checked
}
updateListings() {
if (this.search) {
searchRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSearchQuery(), this.getIncludeDescriptions())
.then(res => {
let table = document.getElementById("pkg-list")!
table.innerHTML = ''
for (let row of res.values!) {
table.appendChild(entryToHTML(row))
}
STATE.maxTotal = res.count!
STATE.updateRange()
if(res.count! < 1) {
exitSearch()
alert("no results found!")
}
})
} else {
getRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSortOrder())
.then(res => {
let table = document.getElementById("pkg-list")!
table.innerHTML = ''
for (let row of res.values!) {
table.appendChild(entryToHTML(row))
}
})
}
}
}
let STATE: State
function lastPageIndex(): number {
return Math.floor(STATE.getMaxTotal() / STATE.getEntriesPerPage()) * STATE.getEntriesPerPage()
}
function setPage(page: number) {
STATE.setEntryIndex(Math.max(0, Math.min(STATE.getEntriesPerPage() * (page - 1), lastPageIndex())))
}
function escapeHtml(str?: string): string {
let out: string = ''
if (str == undefined) return ""
for (let i = 0; i < str.length; i++) {
out += escapeHtmlChar(str[i])
}
return out
}
function escapeHtmlChar(char: string): string {
if (char.length != 1) return char
switch (char[0]) {
case '&':
return "&amp;"
case '<':
return "&lt;"
case '>':
return "&gt;"
case '"':
return "&quot;"
case "'":
return "&apos;"
default:
return char
}
}
function firstPage() {
STATE.setEntryIndex(0)
}
function prevPage() {
let index = STATE.getEntryIndex()
STATE.setEntryIndex(Math.max(0, index - STATE.getEntriesPerPage()))
}
function lastPage() {
STATE.setEntryIndex(lastPageIndex())
}
function nextPage() {
let index = STATE.getEntryIndex()
STATE.setEntryIndex(Math.min(lastPageIndex(), index + STATE.getEntriesPerPage()))
}
function doSearch() {
document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
document.getElementById("search-top-controls")!.toggleAttribute("hidden");
STATE.search = true;
STATE.setEntryIndex(0);
}
function exitSearch() {
document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
document.getElementById("search-top-controls")!.toggleAttribute("hidden");
STATE.search = false;
STATE.setMaxTotal(STATE.maxEntries)
STATE.setEntryIndex(0)
}
function main() {
STATE = new State()
infoRequest()
.then(res => {
STATE.maxEntries = res.count!
STATE.setMaxTotal(STATE.maxEntries)
document.getElementById("hakurei-version")!.textContent = res.hakurei_version!
STATE.updateRange()
STATE.updateListings()
})
for (let e of document.getElementsByClassName("page-number")) {
e.addEventListener("change", (_) => {
setPage(parseInt((e as HTMLInputElement).value))
})
}
document.getElementById("count")?.addEventListener("change", (event) => {
STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value))
})
document.getElementById("sort")?.addEventListener("change", (event) => {
STATE.setSortOrder(parseInt((event.target as HTMLSelectElement).value))
})
document.getElementById("search")?.addEventListener("keyup", (event) => {
if (event.key === 'Enter') doSearch()
})
}
+21
View File
@@ -0,0 +1,21 @@
.page-number {
width: 2em;
text-align: center;
}
.page-number {
width: 2em;
text-align: center;
}
@media (prefers-color-scheme: dark) {
html {
background-color: #2c2c2c;
color: ghostwhite;
}
}
@media (prefers-color-scheme: light) {
html {
background-color: #d3d3d3;
color: black;
}
}
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "ES2024",
"strict": true,
"alwaysStrict": true,
"outDir": "static"
}
}
+9
View File
@@ -0,0 +1,9 @@
// Package ui holds the static web UI.
package ui
import "net/http"
// Register arranges for mux to serve the embedded frontend.
func Register(mux *http.ServeMux) {
mux.Handle("GET /", http.FileServer(http.FS(static)))
}
+21
View File
@@ -0,0 +1,21 @@
//go:build frontend
package ui
import (
"embed"
"io/fs"
)
//go:generate tsc
//go:generate cp index.html style.css static
//go:embed static
var _static embed.FS
var static = func() fs.FS {
if f, err := fs.Sub(_static, "static"); err != nil {
panic(err)
} else {
return f
}
}()
+7
View File
@@ -0,0 +1,7 @@
//go:build !frontend
package ui
import "testing/fstest"
var static fstest.MapFS
+516 -180
View File
@@ -14,12 +14,14 @@ package main
import (
"context"
"crypto/ed25519"
"crypto/sha512"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
@@ -30,19 +32,34 @@ import (
"syscall"
"time"
"unique"
"unsafe"
"hakurei.app/check"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/ext"
"hakurei.app/fhs"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
"hakurei.app/message"
"hakurei.app/cmd/mbf/internal/pkgserver"
"hakurei.app/cmd/mbf/internal/pkgserver/ui"
)
// writeFileExcl is like [os.WriteFile], but sets [os.O_EXCL] instead.
func writeFileExcl(name string, data []byte, perm os.FileMode) error {
f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
if err != nil {
return err
}
_, err = f.Write(data)
if err1 := f.Close(); err1 != nil && err == nil {
err = err1
}
return err
}
func main() {
container.TryArgv0(nil)
@@ -54,34 +71,105 @@ func main() {
log.Fatal("this program must not run as root")
}
defer func() {
r := recover()
if r == nil {
return
}
switch r.(type) {
case rosa.LoadError, pkg.IRStringError:
log.Fatal(r)
default:
panic(r)
}
}()
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer stop()
var cm cache
defer func() {
cm.Close()
if r := recover(); r != nil {
fmt.Println(r)
log.Fatal("consider scrubbing the on-disk cache")
}
}()
defer func() { cm.Close() }()
var (
flagQuiet bool
flagQEMU bool
flagArch string
flagCheck bool
flagLTO bool
flagPT bool
flagDry bool
flagPath string
flagSourcePath string
flagCrossOverride int
addr net.UnixAddr
)
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) error {
if !rosa.Native().HasStageEarly() {
return pkg.UnsupportedArchError(runtime.GOARCH)
}
if flagPT {
log.Println("parsed in", rosa.ParseTime())
}
msg.SwapVerbose(!flagQuiet)
cm.ctx, cm.msg = ctx, msg
cm.base = os.ExpandEnv(cm.base)
if cm.base == "" {
cm.base = "cache"
}
cm.mirror = os.ExpandEnv(cm.mirror)
azaleaPath := os.ExpandEnv(flagPath)
addr.Net = "unix"
addr.Name = os.ExpandEnv(addr.Name)
if addr.Name == "" {
addr.Name = "daemon"
addr.Name = filepath.Join(cm.base, "daemon")
}
var flags int
if !flagCheck {
flags |= rosa.OptSkipCheck
}
if !flagLTO {
flags |= rosa.OptLLVMNoLTO
}
rosa.Native().DropCaches("", flags)
cross := flagArch != "" && flagArch != runtime.GOARCH
if flagQEMU || cross {
_, cm.qemu = rosa.Native().Std().MustLoad(rosa.H("qemu"))
}
if cross {
if flagCrossOverride != -1 {
flags = flagCrossOverride
}
rosa.Native().DropCaches(flagArch, flags)
if !rosa.Native().HasStageEarly() {
return pkg.UnsupportedArchError(flagArch)
}
}
if flagSourcePath != "" {
if err := rosa.Native().SetSource(os.DirFS(flagSourcePath)); err != nil {
return err
}
}
if azaleaPath != "" {
var root *os.Root
if a, err := check.NewAbs(azaleaPath); err != nil {
return err
} else if root, err = os.OpenRoot(a.String()); err != nil {
return err
} else if err = rosa.Native().RegisterFS(root.FS()); err != nil {
return err
}
}
return nil
@@ -89,6 +177,30 @@ func main() {
&flagQuiet,
"q", command.BoolFlag(false),
"Do not print cure messages",
).Flag(
&flagQEMU,
"register", command.BoolFlag(false),
"Enable additional target architectures",
).Flag(
&flagArch,
"arch", command.StringFlag(runtime.GOARCH),
"Target architecture",
).Flag(
&flagLTO,
"lto", command.BoolFlag(false),
"Enable LTO in stage2 and stage3 LLVM toolchains",
).Flag(
&flagCheck,
"check", command.BoolFlag(true),
"Run test suites",
).Flag(
&flagCrossOverride,
"cross-flags", command.IntFlag(-1),
"Override non-native target preset flags",
).Flag(
&cm.verboseInit,
"v", command.BoolFlag(false),
"Do not suppress verbose output from init",
).Flag(
&cm.cures,
"cures", command.IntFlag(0),
@@ -101,6 +213,10 @@ func main() {
&cm.base,
"d", command.StringFlag("$MBF_CACHE_DIR"),
"Directory to store cured artifacts",
).Flag(
&cm.mirror,
"r", command.StringFlag("$MBF_REMOTE"),
"URL of mirror service",
).Flag(
&cm.idle,
"sched-idle", command.BoolFlag(false),
@@ -116,12 +232,38 @@ func main() {
&addr.Name,
"socket", command.StringFlag("$MBF_DAEMON_SOCKET"),
"Pathname of socket to bind to",
).Flag(
&flagPT,
"parse-time", command.BoolFlag(false),
"Print duration of the initial azalea parse",
).Flag(
&flagDry,
"dry", command.BoolFlag(false),
"Do not destroy cache entries",
).Flag(
&flagSourcePath,
"source", command.StringFlag(""),
"Override hakurei source tree",
).Flag(
&flagPath,
"p", command.StringFlag("$AZALEA_PATH"),
"Load additional azalea files",
)
c.NewCommand(
"checksum", "Compute checksum of data read from standard input",
func([]string) error {
go func() { <-ctx.Done(); os.Exit(1) }()
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-ctx.Done():
os.Exit(1)
case <-done:
return
}
}()
h := sha512.New384()
if _, err := io.Copy(h, os.Stdin); err != nil {
return err
@@ -155,6 +297,7 @@ func main() {
{
var (
flagBind string
flagStatus bool
flagReport string
)
@@ -162,8 +305,52 @@ func main() {
"info",
"Display out-of-band metadata of an artifact",
func(args []string) (err error) {
return commandInfo(&cm, args, os.Stdout, flagStatus, flagReport)
const shutdownTimeout = 15 * time.Second
var r *rosa.Report
if flagReport != "" {
if r, err = rosa.OpenReport(flagReport); err != nil {
return err
}
defer func() {
if closeErr := r.Close(); err == nil {
err = closeErr
}
}()
defer r.HandleAccess(&err)()
}
if flagBind == "" {
return commandInfo(&cm, args, os.Stdout, flagStatus, r)
}
var mux http.ServeMux
ui.Register(&mux)
if err = pkgserver.Register(ctx, &mux, r); err != nil {
return
}
server := http.Server{Addr: flagBind, Handler: &mux}
go func() {
<-ctx.Done()
cc, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if _err := server.Shutdown(cc); _err != nil {
log.Fatal(_err)
}
}()
msg.Verbosef("listening on %q", flagBind)
err = server.ListenAndServe()
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
return
},
).Flag(
&flagBind,
"bind", command.StringFlag(""),
"TCP address for the server to listen on",
).Flag(
&flagStatus,
"status", command.BoolFlag(false),
@@ -213,7 +400,10 @@ func main() {
)
{
var flagJobs int
var (
flagJobs int
flagNoBlock bool
)
c.NewCommand("updates", command.UsageInternal, func([]string) error {
var (
errsMu sync.Mutex
@@ -222,16 +412,21 @@ func main() {
n atomic.Uint64
)
w := make(chan rosa.PArtifact)
w := make(chan rosa.ArtifactH)
var wg sync.WaitGroup
for range max(flagJobs, 1) {
wg.Go(func() {
for p := range w {
meta := rosa.GetMetadata(p)
meta, _ := rosa.Native().Std().MustLoad(p)
if meta.ID == 0 {
continue
}
if !flagNoBlock && meta.Blocked != "" {
msg.Verbosef("%s is blocked: %s", meta.Name, meta.Blocked)
continue
}
v, err := meta.GetVersions(ctx)
if err != nil {
errsMu.Lock()
@@ -240,12 +435,9 @@ func main() {
continue
}
if current, latest :=
rosa.Std.Version(p),
meta.GetLatest(v); current != latest {
if latest := meta.GetLatest(v); meta.Version != latest {
n.Add(1)
log.Printf("%s %s < %s", meta.Name, current, latest)
log.Printf("%s %s < %s", meta.Name, meta.Version, latest)
continue
}
@@ -255,9 +447,9 @@ func main() {
}
done:
for i := range rosa.PresetEnd {
for _, p := range rosa.Native().CollectAll() {
select {
case w <- rosa.PArtifact(i):
case w <- p:
break
case <-ctx.Done():
break done
@@ -275,9 +467,23 @@ func main() {
&flagJobs,
"j", command.IntFlag(32),
"Maximum number of simultaneous connections",
).Flag(
&flagNoBlock,
"ignore-block", command.BoolFlag(false),
"Inhibit update blocking",
)
}
c.NewCommand("blocked", command.UsageInternal, func([]string) error {
for _, p := range rosa.Native().CollectAll() {
meta, _ := rosa.Native().Std().Load(p)
if meta.Blocked != "" {
fmt.Printf("%s: %s\n", meta.Name, meta.Blocked)
}
}
return nil
})
c.NewCommand(
"daemon",
"Service artifact IR with Rosa OS extensions",
@@ -291,20 +497,82 @@ func main() {
},
)
c.NewCommand(
"keygen",
"Create keypair for local cache",
func([]string) error {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
return err
}
return errors.Join(writeFileExcl(filepath.Join(
cm.base,
"ed25519.pub",
), pub, 0444), writeFileExcl(filepath.Join(
cm.base,
"ed25519",
), priv, 0400))
},
)
c.NewCommand(
"serve",
"Export local cache as mirror",
func(args []string) error {
const shutdownTimeout = 15 * time.Second
if len(args) != 1 {
return errors.New("serve requires 1 argument")
}
var key ed25519.PrivateKey
if p, err := os.ReadFile(filepath.Join(cm.base, "ed25519")); err != nil {
return err
} else if len(p) != ed25519.PrivateKeySize {
return errors.New("invalid private key")
} else {
key = p
}
var h http.Handler
if base, err := os.OpenRoot(cm.base); err != nil {
return err
} else {
h = rosa.NewMirror(msg, base.FS(), key)
}
server := http.Server{Addr: args[0], Handler: h}
go func() {
<-ctx.Done()
cc, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := server.Shutdown(cc); err != nil {
log.Fatal(err)
}
}()
msg.Verbosef("listening on %q", args[0])
err := server.ListenAndServe()
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
return err
},
)
{
var (
flagGentoo string
flagChecksum string
flagStage0 bool
)
c.NewCommand(
"stage3",
"Check for toolchain 3-stage non-determinism",
func(args []string) (err error) {
t := rosa.Std
s := rosa.Std
if flagGentoo != "" {
t -= 3 // magic number to discourage misuse
s -= 3 // magic number to discourage misuse
var checksum pkg.Checksum
if len(flagChecksum) != 0 {
@@ -312,7 +580,7 @@ func main() {
return
}
}
rosa.SetGentooStage3(flagGentoo, checksum)
rosa.Native().SetGentooStage3(flagGentoo, checksum)
}
var (
@@ -320,10 +588,10 @@ func main() {
checksum [2]unique.Handle[pkg.Checksum]
)
_llvm := rosa.H("llvm")
if err = cm.Do(func(cache *pkg.Cache) (err error) {
pathname, _, err = cache.Cure(
(t - 2).Load(rosa.Clang),
)
_, llvm := rosa.Native().New(s - 2).Load(_llvm)
pathname, _, err = cache.Cure(llvm)
return
}); err != nil {
return
@@ -331,18 +599,16 @@ func main() {
log.Println("stage1:", pathname)
if err = cm.Do(func(cache *pkg.Cache) (err error) {
pathname, checksum[0], err = cache.Cure(
(t - 1).Load(rosa.Clang),
)
_, llvm := rosa.Native().New(s - 1).Load(_llvm)
pathname, checksum[0], err = cache.Cure(llvm)
return
}); err != nil {
return
}
log.Println("stage2:", pathname)
if err = cm.Do(func(cache *pkg.Cache) (err error) {
pathname, checksum[1], err = cache.Cure(
t.Load(rosa.Clang),
)
_, llvm := rosa.Native().New(s).Load(_llvm)
pathname, checksum[1], err = cache.Cure(llvm)
return
}); err != nil {
return
@@ -360,19 +626,6 @@ func main() {
"("+pkg.Encode(checksum[0].Value())+")",
)
}
if flagStage0 {
if err = cm.Do(func(cache *pkg.Cache) (err error) {
pathname, _, err = cache.Cure(
t.Load(rosa.Stage0),
)
return
}); err != nil {
return
}
log.Println(pathname)
}
return
},
).Flag(
@@ -383,10 +636,6 @@ func main() {
&flagChecksum,
"checksum", command.StringFlag(""),
"Checksum of Gentoo stage3 tarball",
).Flag(
&flagStage0,
"stage0", command.BoolFlag(false),
"Create bootstrap stage0 tarball",
)
}
@@ -397,6 +646,11 @@ func main() {
flagExport string
flagRemote bool
flagNoReply bool
flagFaults bool
flagPop bool
flagBoot bool
flagStd bool
)
c.NewCommand(
"cure",
@@ -405,8 +659,16 @@ func main() {
if len(args) != 1 {
return errors.New("cure requires 1 argument")
}
p, ok := rosa.ResolveName(args[0])
if !ok {
t := rosa.Std
if flagBoot {
t -= 2
} else if flagStd {
t -= 1
}
_, a := rosa.Native().New(t).Load(rosa.ArtifactH(unique.Make(args[0])))
if a == nil {
return fmt.Errorf("unknown artifact %q", args[0])
}
@@ -414,7 +676,7 @@ func main() {
default:
var pathname *check.Absolute
err := cm.Do(func(cache *pkg.Cache) (err error) {
pathname, _, err = cache.Cure(rosa.Std.Load(p))
pathname, _, err = cache.Cure(a)
return
})
if err != nil {
@@ -432,7 +694,7 @@ func main() {
0400,
); err != nil {
return err
} else if _, err = pkg.Flatten(
} else if err = pkg.Write(
os.DirFS(pathname.String()),
".",
f,
@@ -456,7 +718,7 @@ func main() {
return err
}
if err = pkg.NewIR().EncodeAll(f, rosa.Std.Load(p)); err != nil {
if err = pkg.NewIR().EncodeAll(f, a); err != nil {
_ = f.Close()
return err
}
@@ -467,8 +729,8 @@ func main() {
return cm.Do(func(cache *pkg.Cache) error {
return cache.EnterExec(
ctx,
rosa.Std.Load(p),
true, os.Stdin, os.Stdout, os.Stderr,
a,
"", true, os.Stdin, os.Stdout, os.Stderr,
rosa.AbsSystem.Append("bin", "mksh"),
"sh",
)
@@ -479,7 +741,6 @@ func main() {
if flagNoReply {
flags |= remoteNoReply
}
a := rosa.Std.Load(p)
pathname, err := cureRemote(ctx, &addr, a, flags)
if !flagNoReply && err == nil {
log.Println(pathname)
@@ -489,12 +750,55 @@ func main() {
cc, cancel := context.WithDeadline(context.Background(), daemonDeadline())
defer cancel()
if _err := cancelRemote(cc, &addr, a); _err != nil {
if _err := cancelRemote(cc, &addr, a, false); _err != nil {
log.Println(err)
}
}
return err
case flagFaults:
var faults []pkg.Fault
if err := cm.Do(func(cache *pkg.Cache) (err error) {
faults, err = cache.ReadFaults(a)
return
}); err != nil {
return err
}
for _, fault := range faults {
log.Printf("%s: %s ago", fault.String(), time.Since(fault.Time()))
}
return nil
case flagPop:
var faults []pkg.Fault
if err := cm.Do(func(cache *pkg.Cache) (err error) {
faults, err = cache.ReadFaults(a)
return
}); err != nil {
return err
}
if len(faults) == 0 {
return errors.New("no fault entries found")
}
fault := faults[len(faults)-1]
r, err := fault.Open()
if err != nil {
return err
}
if _, err = io.Copy(os.Stdout, r); err != nil {
_ = r.Close()
return err
}
fmt.Println()
if err = r.Close(); err != nil {
return err
}
log.Printf("faulting cure terminated %s ago", time.Since(fault.Time()))
return fault.Destroy()
}
},
).Flag(
@@ -517,13 +821,105 @@ func main() {
&flagNoReply,
"no-reply", command.BoolFlag(false),
"Do not receive a reply from the daemon",
).Flag(
&flagBoot,
"boot", command.BoolFlag(false),
"Build on the stage0 toolchain",
).Flag(
&flagStd,
"std", command.BoolFlag(false),
"Build on the intermediate toolchain",
).Flag(
&flagFaults,
"faults", command.BoolFlag(false),
"Display fault entries of the specified artifact",
).Flag(
&flagPop,
"pop", command.BoolFlag(false),
"Display and destroy the most recent fault entry",
)
}
cleanC := c.New("clean", "Remove unused entries from the cache")
cleanC.NewCommand(
"fault",
"Remove all fault entries from the cache",
func([]string) error {
return cm.Do(func(*pkg.Cache) error {
pathname := filepath.Join(cm.base, "fault")
dents, err := os.ReadDir(pathname)
if err != nil {
return err
}
for _, dent := range dents {
msg.Verbosef("destroying entry %s", dent.Name())
if err = os.Remove(filepath.Join(pathname, dent.Name())); err != nil {
return err
}
}
log.Printf("destroyed %d fault entries", len(dents))
return nil
})
},
)
cleanC.NewCommand(
"checksum",
"Remove unreachable checksum entries",
func([]string) error {
return cm.Do(func(cache *pkg.Cache) error {
_, checksums, err := cache.Clean(flagDry, false)
log.Printf("destroyed %d entries", len(checksums))
return err
})
},
)
{
var flagDeep bool
cleanC.NewCommand(
"all",
"Remove identifiers not reachable by loaded packages",
func([]string) error {
return cm.Do(func(cache *pkg.Cache) error {
t := rosa.Native().Clone().Std()
handles := t.CollectAll()
flags := t.Flags()
a := t.Append(nil, handles...)
for arch := range rosa.Arches(nil) {
if arch == runtime.GOARCH {
continue
}
t.DropCaches(arch, rosa.OptLLVMNoLTO|rosa.OptSkipCheck)
a = t.Append(a, handles...)
t.DropCaches(arch, flags)
a = t.Append(a, handles...)
}
ids, checksums, err := cache.Clean(
flagDry,
!flagDeep,
a...,
)
log.Printf(
"destroyed %d identifier and %d checksum entries",
len(ids), len(checksums),
)
return err
})
},
).Flag(
&flagDeep,
"deep", command.BoolFlag(false),
"Include transitive inputs",
)
}
c.NewCommand(
"abort",
"Abort all pending cures on the daemon",
func([]string) error { return abortRemote(ctx, &addr) },
func([]string) error { return abortRemote(ctx, &addr, false) },
)
{
@@ -537,131 +933,72 @@ func main() {
"shell",
"Interactive shell in the specified Rosa OS environment",
func(args []string) error {
presets := make([]rosa.PArtifact, len(args)+3)
for i, arg := range args {
p, ok := rosa.ResolveName(arg)
if !ok {
return fmt.Errorf("unknown artifact %q", arg)
resolvconf := "nameserver 1.1.1.1\nnameserver 1.0.0.1\n"
if p, err := os.ReadFile(fhs.AbsEtc.Append(
"resolv.conf",
).String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err
}
presets[i] = p
} else {
resolvconf = unsafe.String(unsafe.SliceData(p), len(p))
}
base := rosa.Clang
if !flagWithToolchain {
base = rosa.Musl
handles := make([]rosa.ArtifactH, len(args), len(args)+3)
for i, arg := range args {
handles[i] = rosa.ArtifactH(unique.Make(arg))
if meta, _ := rosa.Native().Std().Load(handles[i]); meta == nil {
return fmt.Errorf("unknown artifact %q", arg)
}
}
presets = append(presets,
base := rosa.H("llvm")
if !flagWithToolchain {
base = rosa.H("musl")
}
handles = append(handles,
base,
rosa.Mksh,
rosa.Toybox,
rosa.H("mksh"),
rosa.H("toybox"),
)
root := make(pkg.Collect, 0, 6+len(args))
root = rosa.Std.AppendPresets(root, presets...)
root = append(root, rosa.NewEtc(false))
root = rosa.Native().Std().Append(root, handles...)
if err := cm.Do(func(cache *pkg.Cache) error {
_, _, err := cache.Cure(&root)
return err
}); err == nil {
return errors.New("unreachable")
} else if !pkg.IsCollected(err) {
return err
}
type cureRes struct {
pathname *check.Absolute
checksum unique.Handle[pkg.Checksum]
}
cured := make(map[pkg.Artifact]cureRes)
for _, a := range root {
if err := cm.Do(func(cache *pkg.Cache) error {
pathname, checksum, err := cache.Cure(a)
if err == nil {
cured[a] = cureRes{pathname, checksum}
}
return err
}); err != nil {
return err
}
}
// explicitly open for direct error-free use from this point
if cm.c == nil {
if err := cm.open(); err != nil {
return err
}
}
layers := pkg.PromoteLayers(root, func(a pkg.Artifact) (
*check.Absolute,
unique.Handle[pkg.Checksum],
) {
res := cured[a]
return res.pathname, res.checksum
}, func(i int, d pkg.Artifact) {
r := pkg.Encode(cm.c.Ident(d).Value())
if s, ok := d.(fmt.Stringer); ok {
if name := s.String(); name != "" {
r += "-" + name
}
}
msg.Verbosef("promoted layer %d as %s", i, r)
return cm.Do(func(cache *pkg.Cache) error {
return cache.EnterExec(
ctx,
pkg.NewExec(
"",
rosa.Native().Arch(),
new(pkg.Checksum),
1,
flagNet,
false,
fhs.AbsRoot,
[]string{
"SHELL=/system/bin/mksh",
"PATH=/system/bin",
"HOME=/",
},
fhs.AbsProc.Append("nonexistent"),
nil,
pkg.Path(fhs.AbsRoot, true, root...),
pkg.Path(
fhs.AbsEtc.Append("resolv.conf"), false,
pkg.NewFile(
"resolv.conf",
unsafe.Slice(unsafe.StringData(resolvconf), len(resolvconf)),
),
),
),
"localhost",
flagSession, os.Stdin, os.Stdout, os.Stderr,
rosa.AbsSystem.Append("bin", "mksh"),
"sh",
)
})
z := container.New(ctx, msg)
z.WaitDelay = 3 * time.Second
z.SeccompPresets = pkg.SeccompPresets
z.SeccompFlags |= seccomp.AllowMultiarch
z.ParentPerm = 0700
z.HostNet = flagNet
z.RetainSession = flagSession
z.Hostname = "localhost"
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
if s, ok := os.LookupEnv("TERM"); ok {
z.Env = append(z.Env, "TERM="+s)
}
var tempdir *check.Absolute
if s, err := filepath.Abs(os.TempDir()); err != nil {
return err
} else if tempdir, err = check.NewAbs(s); err != nil {
return err
}
z.Dir = fhs.AbsRoot
z.Env = []string{
"SHELL=/system/bin/mksh",
"PATH=/system/bin",
"HOME=/",
}
z.Path = rosa.AbsSystem.Append("bin", "mksh")
z.Args = []string{"mksh"}
z.
OverlayEphemeral(fhs.AbsRoot, layers...).
Place(
fhs.AbsEtc.Append("hosts"),
[]byte("127.0.0.1 localhost\n"),
).
Place(
fhs.AbsEtc.Append("passwd"),
[]byte("media_rw:x:1023:1023::/:/system/bin/sh\n"+
"nobody:x:65534:65534::/proc/nonexistent:/system/bin/false\n"),
).
Place(
fhs.AbsEtc.Append("group"),
[]byte("media_rw:x:1023:\nnobody:x:65534:\n"),
).
Bind(tempdir, fhs.AbsTmp, std.BindWritable).
Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
if err := z.Start(); err != nil {
return err
}
if err := z.Serve(); err != nil {
return err
}
return z.Wait()
},
).Flag(
&flagNet,
@@ -676,7 +1013,6 @@ func main() {
"with-toolchain", command.BoolFlag(false),
"Include the stage2 LLVM toolchain",
)
}
c.Command(
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"net"
"os"
"testing"
"hakurei.app/internal/rosa"
)
func TestMain(m *testing.M) {
rosa.Native().DropCaches("", rosa.OptLLVMNoLTO)
os.Exit(m.Run())
}
func TestCureAll(t *testing.T) {
t.Parallel()
const env = "ROSA_TEST_DAEMON"
if !testing.Verbose() {
t.Skip("verbose flag not set")
}
pathname, ok := os.LookupEnv(env)
if !ok {
t.Skip(env + " not set")
}
addr := net.UnixAddr{Net: "unix", Name: pathname}
t.Cleanup(func() {
if t.Failed() {
if err := abortRemote(t.Context(), &addr, false); err != nil {
t.Fatal(err)
}
}
})
for _, handle := range rosa.Native().Collect() {
_, a := rosa.Native().Std().MustLoad(handle)
t.Run(handle.String(), func(t *testing.T) {
_, err := cureRemote(t.Context(), &addr, a, 0)
if err != nil {
t.Error(err)
}
})
}
}
+2 -2
View File
@@ -508,8 +508,8 @@ func _main(s ...string) (exitCode int) {
if !z.AllowOrphan {
if err := z.Wait(); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) || exitError == nil {
exitError, ok := errors.AsType[*exec.ExitError](err)
if !ok || exitError == nil {
log.Println(err)
return 5
}
+4 -1
View File
@@ -20,11 +20,14 @@
};
virtualisation = {
# Hopefully reduces spurious test failures:
memorySize = if pkgs.stdenv.hostPlatform.is32bit then 2046 else 8192;
diskSize = 6 * 1024;
qemu.options = [
# Increase test performance:
"-smp 8"
"-smp 16"
];
};
+1 -1
View File
@@ -28,7 +28,7 @@ testers.nixosTest {
# For go tests:
(pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" ''
cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei"
${fhs}/bin/hakurei-fhs -c 'CC="clang -O3 -Werror" go test ./...'
${fhs}/bin/hakurei-fhs -c 'ROSA_SKIP_BINFMT=1 CC="clang -O3 -Werror" go test ./...'
'')
];
+2 -2
View File
@@ -91,8 +91,8 @@ func (n *node) MustParse(arguments []string, handleError func(error)) {
case ErrEmptyTree:
os.Exit(1)
default:
var flagError FlagError
if !errors.As(err, &flagError) { // returned by HandlerFunc
flagError, ok := errors.AsType[FlagError](err)
if !ok { // returned by HandlerFunc
handleError(err)
os.Exit(1)
}
+46
View File
@@ -0,0 +1,46 @@
package container
import (
"strings"
"unsafe"
"hakurei.app/check"
)
// escapeBinfmt escapes magic/mask sequences in a [BinfmtEntry].
func escapeBinfmt(buf *strings.Builder, s string) string {
const lowerhex = "0123456789abcdef"
buf.Reset()
for _, c := range unsafe.Slice(unsafe.StringData(s), len(s)) {
switch c {
case 0, '\\', ':':
buf.WriteString(`\x`)
buf.WriteByte(lowerhex[c>>4])
buf.WriteByte(lowerhex[c&0xf])
default:
buf.WriteByte(c)
}
}
return buf.String()
}
// BinfmtEntry is an entry to be registered by the init process.
type BinfmtEntry struct {
// The offset of the magic/mask in the file, counted in bytes.
Offset byte
// The byte sequence binfmt_misc is matching for.
Magic string
// An (optional, defaults to all 0xff) mask.
Mask string
// The program that should be invoked with the binary as first argument.
Interpreter *check.Absolute
}
// Valid returns whether e can be registered into the kernel.
func (e *BinfmtEntry) Valid() bool {
return e != nil &&
int(e.Offset)+max(len(e.Magic), len(e.Mask)) < 128 &&
e.Interpreter != nil && len(e.Interpreter.String()) < 128
}
+62
View File
@@ -0,0 +1,62 @@
package container
import (
"strings"
"testing"
"hakurei.app/fhs"
)
func TestEscapeBinfmt(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
magic string
want string
}{
{"packed DOS applications", "\x0eDEX", "\x0eDEX"},
{"riscv64 magic",
"\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00",
"\x7fELF\x02\x01\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\x02\\x00\xf3\\x00"},
{"riscv64 mask",
"\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
"\xff\xff\xff\xff\xff\xff\xff\\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := escapeBinfmt(new(strings.Builder), tc.magic)
if got != tc.want {
t.Errorf("escapeBinfmt: %q, want %q", got, tc.want)
}
})
}
}
func TestBinfmtEntry(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
e BinfmtEntry
valid bool
}{
{"zero", BinfmtEntry{}, false},
{"large offset", BinfmtEntry{Offset: 128}, false},
{"long magic", BinfmtEntry{Magic: strings.Repeat("\x00", 128)}, false},
{"long mask", BinfmtEntry{Mask: strings.Repeat("\x00", 128)}, false},
{"valid", BinfmtEntry{Interpreter: fhs.AbsRoot}, true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if tc.e.Valid() != tc.valid {
t.Errorf("Valid: %v", !tc.valid)
}
})
}
}
+1
View File
@@ -18,6 +18,7 @@ const (
CAP_SETPCAP = 0x8
CAP_NET_ADMIN = 0xc
CAP_DAC_OVERRIDE = 0x1
CAP_SETFCAP = 0x1f
)
type (
+29 -11
View File
@@ -67,6 +67,9 @@ type (
// Copied to the underlying [exec.Cmd].
WaitDelay time.Duration
// Suppress verbose output of init.
Quiet bool
cmd *exec.Cmd
ctx context.Context
msg message.Msg
@@ -88,12 +91,20 @@ type (
// Time to wait for processes lingering after the initial process terminates.
AdoptWaitDelay time.Duration
// Map uid/gid 0 in the init process. Requires [FstypeProc] attached to
// [fhs.Proc] in the container filesystem.
InitAsRoot bool
// Mapped Uid in user namespace.
Uid int
// Mapped Gid in user namespace.
Gid int
// Hostname value in UTS namespace.
Hostname string
// Register binfmt_misc entries.
Binfmt []BinfmtEntry
// Alternative pathname to attach binfmt_misc filesystem. The zero value
// requires [FstypeProc] to be made available at [fhs.Proc].
BinfmtPath *check.Absolute
// Sequential container setup ops.
*Ops
@@ -143,11 +154,8 @@ func (e *StartError) Error() string {
return e.Step
}
{
var syscallError *os.SyscallError
if errors.As(e.Err, &syscallError) && syscallError != nil {
return e.Step + " " + syscallError.Error()
}
if se, ok := errors.AsType[*os.SyscallError](e.Err); ok && se != nil {
return e.Step + " " + se.Error()
}
return e.Step + ": " + e.Err.Error()
@@ -213,6 +221,9 @@ func (p *Container) Start() error {
if p.cmd.Process != nil {
return errors.New("container: already started")
}
if !p.InitAsRoot && len(p.Binfmt) > 0 {
return errors.New("container: init as root required, but not enabled")
}
if err := ensureCloseOnExec(); err != nil {
return err
@@ -283,6 +294,18 @@ func (p *Container) Start() error {
if !p.HostNet {
p.cmd.SysProcAttr.Cloneflags |= CLONE_NEWNET
}
if p.InitAsRoot {
p.cmd.SysProcAttr.AmbientCaps = append(p.cmd.SysProcAttr.AmbientCaps,
// mappings during init as root
CAP_SETFCAP,
)
if !p.SeccompDisable &&
len(p.SeccompRules) == 0 &&
p.SeccompPresets&std.PresetDenyNS != 0 {
return errors.New("container: as root requires late namespace creation")
}
}
// place setup pipe before user supplied extra files, this is later restored by init
if r, w, err := os.Pipe(); err != nil {
@@ -342,8 +365,6 @@ func (p *Container) Start() error {
Err: ENOSYS,
Origin: true,
}
} else {
p.msg.Verbosef("landlock abi version %d", abi)
}
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
@@ -353,7 +374,6 @@ func (p *Container) Start() error {
Err: err,
}
} else {
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = landlock.RestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd)
return &StartError{
@@ -410,7 +430,6 @@ func (p *Container) Start() error {
}
}
p.msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil {
return &StartError{
Step: "start container init",
@@ -481,7 +500,6 @@ func (p *Container) Serve() (err error) {
}
case <-done:
p.msg.Verbose("setup payload took", time.Since(t))
return
}
}(p.setup[1])
@@ -491,7 +509,7 @@ func (p *Container) Serve() (err error) {
Getuid(),
Getgid(),
len(p.ExtraFiles),
p.msg.IsVerbose(),
p.msg.IsVerbose() && !p.Quiet,
})
}
+203 -81
View File
@@ -16,6 +16,8 @@ import (
"strings"
"syscall"
"testing"
"time"
"unsafe"
"hakurei.app/check"
"hakurei.app/command"
@@ -233,6 +235,9 @@ func earlyMnt(mnt ...*vfs.MountInfoEntry) func(*testing.T, context.Context) []*v
return func(*testing.T, context.Context) []*vfs.MountInfoEntry { return mnt }
}
//go:linkname toHost hakurei.app/container.toHost
func toHost(name string) string
var containerTestCases = []struct {
name string
filter bool
@@ -332,13 +337,15 @@ var containerTestCases = []struct {
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
"rw,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
"rw"+
",lowerdir+="+
toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+
",lowerdir+="+
toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
",upperdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*check.Absolute).String())+
toHost(ctx.Value(testVal("upper")).(*check.Absolute).String())+
",workdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*check.Absolute).String())+
toHost(ctx.Value(testVal("work")).(*check.Absolute).String())+
",redirect_dir=nofollow,uuid=on,userxattr"),
}
},
@@ -388,9 +395,11 @@ var containerTestCases = []struct {
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
"ro,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
"ro"+
",lowerdir+="+
toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+
",lowerdir+="+
toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
",redirect_dir=nofollow,userxattr"),
}
},
@@ -400,39 +409,11 @@ var containerTestCases = []struct {
func TestContainer(t *testing.T) {
t.Parallel()
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled
wantExitCode := 0
if err := c.Wait(); !reflect.DeepEqual(err, wantErr) {
if m, ok := container.InternalMessageFromError(err); ok {
t.Error(m)
}
t.Errorf("Wait: error = %#v, want %#v", err, wantErr)
}
if ps := c.ProcessState(); ps == nil {
t.Errorf("ProcessState unexpectedly returned nil")
} else if code := ps.ExitCode(); code != wantExitCode {
t.Errorf("ExitCode: %d, want %d", code, wantExitCode)
}
}))
t.Run("forward", testContainerCancel(func(c *container.Container) {
c.ForwardCancel = true
}, func(t *testing.T, c *container.Container) {
var exitError *exec.ExitError
if err := c.Wait(); !errors.As(err, &exitError) {
if m, ok := container.InternalMessageFromError(err); ok {
t.Error(m)
}
t.Errorf("Wait: error = %v", err)
}
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
}
}))
var suffix string
runTests:
for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) {
_suffix := suffix
t.Run(tc.name+_suffix, func(t *testing.T) {
t.Parallel()
wantOps, wantOpsCtx := tc.ops(t)
@@ -456,6 +437,8 @@ func TestContainer(t *testing.T) {
c.SeccompDisable = !tc.filter
c.RetainSession = tc.session
c.HostNet = tc.net
c.InitAsRoot = _suffix != ""
c.Env = append(c.Env, "HAKUREI_TEST_SUFFIX="+_suffix)
if info.CanDegrade {
if _, err := landlock.GetABI(); err != nil {
if !errors.Is(err, syscall.ENOSYS) {
@@ -465,6 +448,9 @@ func TestContainer(t *testing.T) {
t.Log("Landlock LSM is unavailable, enabling HostAbstract")
}
}
if c.InitAsRoot {
c.SeccompPresets &= ^std.PresetDenyNS
}
c.
Readonly(check.MustAbs(pathReadonly), 0755).
@@ -533,6 +519,11 @@ func TestContainer(t *testing.T) {
}
})
}
if suffix == "" {
suffix = " as root"
goto runTests
}
}
func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
@@ -555,49 +546,118 @@ func hostnameFromTestCase(name string) string {
}
func testContainerCancel(
t *testing.T,
containerExtra func(c *container.Container),
waitCheck func(t *testing.T, c *container.Container),
) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(t.Context())
waitCheck func(ps *os.ProcessState, waitErr error),
) {
ctx, cancel := context.WithCancel(t.Context())
c := helperNewContainer(ctx, "block")
c.Stdout, c.Stderr = os.Stdout, os.Stderr
if containerExtra != nil {
containerExtra(c)
}
ready := make(chan struct{})
if r, w, err := os.Pipe(); err != nil {
t.Fatalf("cannot pipe: %v", err)
} else {
c.ExtraFiles = append(c.ExtraFiles, w)
go func() {
defer close(ready)
if _, err = r.Read(make([]byte, 1)); err != nil {
panic(err.Error())
}
}()
}
if err := c.Start(); err != nil {
if m, ok := container.InternalMessageFromError(err); ok {
t.Fatal(m)
} else {
t.Fatalf("cannot start container: %v", err)
}
} else if err = c.Serve(); err != nil {
if m, ok := container.InternalMessageFromError(err); ok {
t.Error(m)
} else {
t.Errorf("cannot serve setup params: %v", err)
}
}
<-ready
cancel()
waitCheck(t, c)
c := helperNewContainer(ctx, "block")
c.Stdout, c.Stderr = os.Stdout, os.Stderr
if containerExtra != nil {
containerExtra(c)
}
ready := make(chan struct{})
var waitErr error
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("cannot pipe: %v", err)
}
c.ExtraFiles = append(c.ExtraFiles, w)
go func() {
defer close(ready)
if _, _err := r.Read(make([]byte, 1)); _err != nil {
panic(_err)
}
}()
if err = c.Start(); err != nil {
if m, ok := container.InternalMessageFromError(err); ok {
t.Fatal(m)
} else {
t.Fatalf("cannot start container: %v", err)
}
}
done := make(chan struct{})
go func() {
defer close(done)
waitErr = c.Wait()
_ = r.SetReadDeadline(time.Now())
}()
if err = c.Serve(); err != nil {
if m, ok := container.InternalMessageFromError(err); ok {
t.Error(m)
} else {
t.Errorf("cannot serve setup params: %v", err)
}
}
<-ready
cancel()
<-done
waitCheck(c.ProcessState(), waitErr)
}
func TestForward(t *testing.T) {
t.Parallel()
f := func(ps *os.ProcessState, waitErr error) {
var exitError *exec.ExitError
if !errors.As(waitErr, &exitError) {
if m, ok := container.InternalMessageFromError(waitErr); ok {
t.Error(m)
}
t.Errorf("Wait: error = %v", waitErr)
}
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
}
}
t.Run("direct", func(t *testing.T) {
t.Parallel()
testContainerCancel(t, func(c *container.Container) {
c.ForwardCancel = true
}, f)
})
t.Run("as root", func(t *testing.T) {
testContainerCancel(t, func(c *container.Container) {
c.ForwardCancel = true
c.InitAsRoot = true
c.Proc(fhs.AbsProc)
}, f)
})
}
func TestCancel(t *testing.T) {
t.Parallel()
f := func(ps *os.ProcessState, waitErr error) {
wantErr := context.Canceled
if !reflect.DeepEqual(waitErr, wantErr) {
if m, ok := container.InternalMessageFromError(waitErr); ok {
t.Error(m)
}
t.Errorf("Wait: error = %#v, want %#v", waitErr, wantErr)
}
if ps == nil {
t.Errorf("ProcessState unexpectedly returned nil")
} else if code := ps.ExitCode(); code != 0 {
t.Errorf("ExitCode: %d, want %d", code, 0)
}
}
t.Run("direct", func(t *testing.T) {
t.Parallel()
testContainerCancel(t, nil, f)
})
t.Run("as root", func(t *testing.T) {
testContainerCancel(t, func(c *container.Container) {
c.InitAsRoot = true
c.Proc(fhs.AbsProc)
}, f)
})
}
func TestContainerString(t *testing.T) {
@@ -633,6 +693,8 @@ func init() {
})
c.Command("container", command.UsageInternal, func(args []string) error {
asRoot := os.Getenv("HAKUREI_TEST_SUFFIX") == " as root"
if len(args) != 1 {
return syscall.EINVAL
}
@@ -650,6 +712,66 @@ func init() {
return fmt.Errorf("gid: %d, want %d", gid, tc.gid)
}
// no attack surface increase during as root due to no_new_privs
var wantBounding uintptr = 1
asRootNot := " not"
if !asRoot {
wantBounding = 0
asRootNot = ""
}
const (
PR_CAP_AMBIENT = 0x2f
PR_CAP_AMBIENT_IS_SET = 0x1
)
for i := range container.LastCap(nil) + 1 {
r, _, errno := syscall.Syscall(
syscall.SYS_PRCTL,
PR_CAP_AMBIENT,
PR_CAP_AMBIENT_IS_SET,
i,
)
if errno != 0 {
return os.NewSyscallError("prctl", errno)
}
if r != 0 {
return fmt.Errorf("capability %d in ambient set", i)
}
r, _, errno = syscall.Syscall(
syscall.SYS_PRCTL,
syscall.PR_CAPBSET_READ,
i,
0,
)
if errno != 0 {
return os.NewSyscallError("prctl", errno)
}
if r != wantBounding {
return fmt.Errorf("capability %d%s in bounding set", i, asRootNot)
}
}
const _LINUX_CAPABILITY_VERSION_3 = 0x20080522
var capData struct {
effective uint32
permitted uint32
inheritable uint32
}
if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&struct {
version uint32
pid int32
}{_LINUX_CAPABILITY_VERSION_3, 0})), uintptr(unsafe.Pointer(&capData)), 0); errno != 0 {
return os.NewSyscallError("capget", errno)
}
if max(capData.effective, capData.permitted, capData.inheritable) != 0 {
return fmt.Errorf(
"effective = %d, permitted = %d, inheritable = %d",
capData.effective, capData.permitted, capData.inheritable,
)
}
wantHost := hostnameFromTestCase(tc.name)
if host, err := os.Hostname(); err != nil {
return fmt.Errorf("cannot get hostname: %v", err)
@@ -767,7 +889,7 @@ func TestMain(m *testing.M) {
}
c.MustParse(os.Args[1:], func(err error) {
if err != nil {
log.Fatal(err.Error())
log.Fatal(err)
}
})
return
+5
View File
@@ -65,6 +65,8 @@ type syscallDispatcher interface {
remount(msg message.Msg, target string, flags uintptr) error
// mountTmpfs provides mountTmpfs.
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
// mountOverlay provides mountOverlay.
mountOverlay(target string, options [][2]string) error
// ensureFile provides ensureFile.
ensureFile(name string, perm, pperm os.FileMode) error
// mustLoopback provides mustLoopback.
@@ -169,6 +171,9 @@ func (direct) remount(msg message.Msg, target string, flags uintptr) error {
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
return mountTmpfs(k, fsname, target, flags, size, perm)
}
func (k direct) mountOverlay(target string, options [][2]string) error {
return mountOverlay(target, options)
}
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
return ensureFile(name, perm, pperm)
}
+8 -2
View File
@@ -235,8 +235,6 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
})
}
func sliceAddr[S any](s []S) *[]S { return &s }
func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile {
f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr}
// check happens in Close, and cleanup is not guaranteed to run, so relying
@@ -468,6 +466,14 @@ func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm
stub.CheckArg(k.Stub, "perm", perm, 4))
}
func (k *kstub) mountOverlay(target string, options [][2]string) error {
k.Helper()
return k.Expects("mountOverlay").Error(
stub.CheckArg(k.Stub, "target", target, 0),
stub.CheckArgReflect(k.Stub, "options", options, 1),
)
}
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
k.Helper()
return k.Expects("ensureFile").Error(
+10 -8
View File
@@ -46,9 +46,8 @@ func messageFromError(err error) (m string, ok bool) {
// While this is usable for pointer errors, such use should be avoided as nil
// check is omitted.
func messagePrefix[T error](prefix string, err error) (string, bool) {
var targetError T
if errors.As(err, &targetError) {
return prefix + targetError.Error(), true
if e, ok := errors.AsType[T](err); ok {
return prefix + e.Error(), true
}
return zeroString, false
}
@@ -58,9 +57,8 @@ func messagePrefixP[V any, T interface {
*V
error
}](prefix string, err error) (string, bool) {
var targetError T
if errors.As(err, &targetError) && targetError != nil {
return prefix + targetError.Error(), true
if e, ok := errors.AsType[T](err); ok && e != nil {
return prefix + e.Error(), true
}
return zeroString, false
}
@@ -109,8 +107,8 @@ func optionalErrorUnwrap(err error) error {
// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback.
func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
var errno syscall.Errno
if !errors.As(err, &errno) {
errno, ok := errors.AsType[syscall.Errno](err)
if !ok {
return 0, &os.PathError{Op: op, Path: path, Err: err}
}
return errno, nil
@@ -118,6 +116,10 @@ func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
// mount wraps syscall.Mount for error handling.
func mount(source, target, fstype string, flags uintptr, data string) error {
if max(len(source), len(target), len(data))+1 > os.Getpagesize() {
return &MountError{source, target, fstype, flags, data, syscall.ENOMEM}
}
err := syscall.Mount(source, target, fstype, flags, data)
if err == nil {
return nil
+106 -23
View File
@@ -11,11 +11,13 @@ import (
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
. "syscall"
"time"
"hakurei.app/check"
"hakurei.app/container/seccomp"
"hakurei.app/ext"
"hakurei.app/fhs"
@@ -182,23 +184,33 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
cancel()
}
uid, gid := param.Uid, param.Gid
if param.InitAsRoot {
uid, gid = 0, 0
}
// write uid/gid map here so parent does not need to set dumpable
if err := k.setDumpable(ext.SUID_DUMP_USER); err != nil {
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
}
if err := k.writeFile(fhs.Proc+"self/uid_map",
append([]byte{}, strconv.Itoa(param.Uid)+" "+strconv.Itoa(param.HostUid)+" 1\n"...),
0); err != nil {
if err := k.writeFile(
fhs.Proc+"self/uid_map",
[]byte(strconv.Itoa(uid)+" "+strconv.Itoa(param.HostUid)+" 1\n"),
0,
); err != nil {
k.fatalf(msg, "%v", err)
}
if err := k.writeFile(fhs.Proc+"self/setgroups",
if err := k.writeFile(
fhs.Proc+"self/setgroups",
[]byte("deny\n"),
0); err != nil && !os.IsNotExist(err) {
0,
); err != nil && !os.IsNotExist(err) {
k.fatalf(msg, "%v", err)
}
if err := k.writeFile(fhs.Proc+"self/gid_map",
append([]byte{}, strconv.Itoa(param.Gid)+" "+strconv.Itoa(param.HostGid)+" 1\n"...),
0); err != nil {
[]byte(strconv.Itoa(gid)+" "+strconv.Itoa(param.HostGid)+" 1\n"),
0,
); err != nil {
k.fatalf(msg, "%v", err)
}
if err := k.setDumpable(ext.SUID_DUMP_DISABLE); err != nil {
@@ -223,6 +235,23 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
state := &setupState{process: make(map[int]WaitStatus), Params: &param.Params, Msg: msg, Context: ctx}
defer cancel()
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
k.fatalf(msg, "cannot mount intermediate root: %v", optionalErrorUnwrap(err))
}
if err := k.chdir(intermediateHostPath); err != nil {
k.fatalf(msg, "cannot enter intermediate host path: %v", err)
}
if len(param.Binfmt) > 0 {
for i, e := range param.Binfmt {
if pathname, err := k.evalSymlinks(e.Interpreter.String()); err != nil {
k.fatal(msg, err)
} else if param.Binfmt[i].Interpreter, err = check.NewAbs(pathname); err != nil {
k.fatal(msg, err)
}
}
}
/* early is called right before pivot_root into intermediate root;
this step is mostly for gathering information that would otherwise be
difficult to obtain via library functions after pivot_root, and
@@ -242,13 +271,6 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
}
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
k.fatalf(msg, "cannot mount intermediate root: %v", optionalErrorUnwrap(err))
}
if err := k.chdir(intermediateHostPath); err != nil {
k.fatalf(msg, "cannot enter intermediate host path: %v", err)
}
if err := k.mkdir(sysrootDir, 0755); err != nil {
k.fatalf(msg, "%v", err)
}
@@ -285,6 +307,48 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
}
if len(param.Binfmt) > 0 {
const interpreter = "/interpreter"
if param.BinfmtPath == nil {
param.BinfmtPath = fhs.AbsProcSys.Append("fs/binfmt_misc")
}
binfmt := sysrootPath + param.BinfmtPath.String()
if err := k.mkdirAll(binfmt, 0); err != nil {
k.fatal(msg, err)
}
if err := k.mount(
SourceBinfmtMisc,
binfmt,
FstypeBinfmtMisc,
MS_NOSUID|MS_NOEXEC|MS_NODEV,
zeroString,
); err != nil {
k.fatal(msg, err)
}
var buf strings.Builder
buf.Grow(1920)
register := binfmt + "/register"
for i, e := range param.Binfmt {
if err := k.symlink(hostPath+e.Interpreter.String(), interpreter); err != nil {
k.fatal(msg, err)
} else if err = k.writeFile(register, []byte(":"+
strconv.Itoa(i)+":"+
"M:"+
strconv.Itoa(int(e.Offset))+":"+
escapeBinfmt(&buf, e.Magic)+":"+
escapeBinfmt(&buf, e.Mask)+":"+
interpreter+":"+
"F"), 0); err != nil {
k.fatal(msg, err)
} else if err = k.remove(interpreter); err != nil {
k.fatal(msg, err)
}
}
}
// setup requiring host root complete at this point
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
k.fatalf(msg, "cannot make host root rprivate: %v", optionalErrorUnwrap(err))
@@ -323,11 +387,19 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
}
var keepCaps []uintptr
if param.Privileged {
keepCaps = append(keepCaps, CAP_SYS_ADMIN, CAP_SETPCAP)
}
if param.InitAsRoot {
keepCaps = append(keepCaps, CAP_SETFCAP)
}
if err := k.capAmbientClearAll(); err != nil {
k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
}
for i := uintptr(0); i <= lastcap; i++ {
if param.Privileged && i == CAP_SYS_ADMIN {
for i := range lastcap + 1 {
if slices.Contains(keepCaps, i) {
continue
}
if err := k.capBoundingSetDrop(i); err != nil {
@@ -336,20 +408,23 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
var keep [2]uint32
if param.Privileged {
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
k.fatalf(msg, "cannot raise CAP_SYS_ADMIN: %v", err)
}
for _, c := range keepCaps {
keep[capToIndex(c)] |= capToMask(c)
}
if err := k.capset(
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
&[2]capData{{keep[0], keep[0], keep[0]}, {keep[1], keep[1], keep[1]}},
); err != nil {
k.fatalf(msg, "cannot capset: %v", err)
}
for _, c := range keepCaps {
if err := k.capAmbientRaise(c); err != nil {
k.fatalf(msg, "cannot raise %#x: %v", c, err)
}
}
if !param.SeccompDisable {
rules := param.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets
@@ -474,6 +549,14 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
cmd.ExtraFiles = extraFiles
cmd.Dir = param.Dir.String()
if param.InitAsRoot {
cmd.SysProcAttr = &SysProcAttr{
Cloneflags: CLONE_NEWUSER,
UidMappings: []SysProcIDMap{{ContainerID: param.Uid, HostID: 0, Size: 1}},
GidMappings: []SysProcIDMap{{ContainerID: param.Gid, HostID: 0, Size: 1}},
}
}
msg.Verbosef("starting initial process %s", param.Path)
if err := k.start(cmd); err != nil {
k.fatalf(msg, "%v", err)
+81 -81
View File
@@ -95,7 +95,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -123,7 +123,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -152,7 +152,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -182,7 +182,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -213,7 +213,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -245,7 +245,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -279,7 +279,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -315,7 +315,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -332,6 +332,8 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("fatalf", stub.ExpectArgs{"invalid op at index %d", []any{0}}, nil, nil),
/* end early */
@@ -370,6 +372,8 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("fatalf", stub.ExpectArgs{"invalid op at index %d", []any{0}}, nil, nil),
/* end early */
@@ -408,6 +412,8 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", stub.UniqueError(61)),
call("fatalf", stub.ExpectArgs{"cannot prepare op at index %d: %v", []any{0, stub.UniqueError(61)}}, nil, nil),
@@ -447,6 +453,8 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", &os.PathError{Op: "readlink", Path: "/", Err: stub.UniqueError(60)}),
call("fatal", stub.ExpectArgs{[]any{"cannot readlink /: unique error 60 injected by the test suite"}}, nil, nil),
@@ -486,9 +494,6 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, stub.UniqueError(58)),
call("fatalf", stub.ExpectArgs{"cannot mount intermediate root: %v", []any{stub.UniqueError(58)}}, nil, nil),
},
@@ -526,9 +531,6 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, stub.UniqueError(56)),
call("fatalf", stub.ExpectArgs{"cannot enter intermediate host path: %v", []any{stub.UniqueError(56)}}, nil, nil),
@@ -567,11 +569,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, stub.UniqueError(54)),
call("fatalf", stub.ExpectArgs{"%v", []any{stub.UniqueError(54)}}, nil, nil),
},
@@ -609,11 +611,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, stub.UniqueError(52)),
call("fatalf", stub.ExpectArgs{"cannot bind sysroot: %v", []any{stub.UniqueError(52)}}, nil, nil),
@@ -652,11 +654,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, stub.UniqueError(50)),
@@ -696,11 +698,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -741,11 +743,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -787,11 +789,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -842,11 +844,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -897,11 +899,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -953,11 +955,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1010,11 +1012,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1069,11 +1071,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1129,11 +1131,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1190,11 +1192,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1252,11 +1254,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1315,11 +1317,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1379,11 +1381,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1444,11 +1446,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1510,11 +1512,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1584,11 +1586,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1622,7 +1624,6 @@ func TestInitEntrypoint(t *testing.T) {
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x5)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x6)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x7)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x8)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x9)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xa)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xb)}, nil, nil),
@@ -1654,8 +1655,9 @@ func TestInitEntrypoint(t *testing.T) {
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x26)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x27)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x28)}, nil, nil),
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0x200100, 0x200100, 0x200100}, {0, 0, 0}}}, nil, nil),
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x15)}, nil, stub.UniqueError(19)),
call("fatalf", stub.ExpectArgs{"cannot raise CAP_SYS_ADMIN: %v", []any{stub.UniqueError(19)}}, nil, nil),
call("fatalf", stub.ExpectArgs{"cannot raise %#x: %v", []any{uintptr(0x15), stub.UniqueError(19)}}, nil, nil),
},
}, nil},
@@ -1691,11 +1693,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1729,7 +1731,6 @@ func TestInitEntrypoint(t *testing.T) {
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x5)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x6)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x7)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x8)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x9)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xa)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xb)}, nil, nil),
@@ -1761,8 +1762,7 @@ func TestInitEntrypoint(t *testing.T) {
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x26)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x27)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x28)}, nil, nil),
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x15)}, nil, nil),
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0, 0x200000, 0x200000}, {0, 0, 0}}}, nil, stub.UniqueError(17)),
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0x200100, 0x200100, 0x200100}, {0, 0, 0}}}, nil, stub.UniqueError(17)),
call("fatalf", stub.ExpectArgs{"cannot capset: %v", []any{stub.UniqueError(17)}}, nil, nil),
},
}, nil},
@@ -1799,11 +1799,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -1837,7 +1837,6 @@ func TestInitEntrypoint(t *testing.T) {
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x5)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x6)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x7)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x8)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x9)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xa)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xb)}, nil, nil),
@@ -1869,8 +1868,9 @@ func TestInitEntrypoint(t *testing.T) {
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x26)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x27)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x28)}, nil, nil),
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0x200100, 0x200100, 0x200100}, {0, 0, 0}}}, nil, nil),
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x15)}, nil, nil),
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0, 0x200000, 0x200000}, {0, 0, 0}}}, nil, nil),
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x8)}, nil, nil),
call("verbosef", stub.ExpectArgs{"resolving presets %#x", []any{std.FilterPreset(0xf)}}, nil, nil),
call("seccompLoad", stub.ExpectArgs{seccomp.Preset(0xf, 0), seccomp.ExportFlag(0)}, nil, stub.UniqueError(15)),
call("fatalf", stub.ExpectArgs{"cannot load syscall filter: %v", []any{stub.UniqueError(15)}}, nil, nil),
@@ -1908,11 +1908,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -2032,11 +2032,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -2132,11 +2132,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -2232,11 +2232,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -2323,11 +2323,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -2418,11 +2418,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -2520,11 +2520,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -2659,11 +2659,11 @@ func TestInitEntrypoint(t *testing.T) {
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
@@ -2697,7 +2697,6 @@ func TestInitEntrypoint(t *testing.T) {
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x5)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x6)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x7)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x8)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x9)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xa)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xb)}, nil, nil),
@@ -2729,8 +2728,9 @@ func TestInitEntrypoint(t *testing.T) {
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x26)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x27)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x28)}, nil, nil),
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0x200100, 0x200100, 0x200100}, {0, 0, 0}}}, nil, nil),
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x15)}, nil, nil),
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0, 0x200000, 0x200000}, {0, 0, 0}}}, nil, nil),
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x8)}, nil, nil),
call("verbosef", stub.ExpectArgs{"resolving presets %#x", []any{std.FilterPreset(0xf)}}, nil, nil),
call("seccompLoad", stub.ExpectArgs{seccomp.Preset(0xf, 0), seccomp.ExportFlag(0)}, nil, nil),
call("verbosef", stub.ExpectArgs{"%d filter rules loaded", []any{73}}, nil, nil),
+40 -12
View File
@@ -4,9 +4,9 @@ import (
"encoding/gob"
"fmt"
"slices"
"strings"
"hakurei.app/check"
"hakurei.app/ext"
"hakurei.app/fhs"
)
@@ -150,7 +150,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
return err
} else {
o.upper = check.EscapeOverlayDataSegment(toHost(v))
o.upper = toHost(v)
}
}
@@ -158,7 +158,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
return err
} else {
o.work = check.EscapeOverlayDataSegment(toHost(v))
o.work = toHost(v)
}
}
}
@@ -168,12 +168,39 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if v, err := k.evalSymlinks(a.String()); err != nil {
return err
} else {
o.lower[i] = check.EscapeOverlayDataSegment(toHost(v))
o.lower[i] = toHost(v)
}
}
return nil
}
// mountOverlay sets up an overlay mount via [ext.FS].
func mountOverlay(target string, options [][2]string) error {
fs, err := ext.OpenFS(SourceOverlay, 0)
if err != nil {
return err
}
if err = fs.SetString("source", SourceOverlay); err != nil {
_ = fs.Close()
return err
}
for _, option := range options {
if err = fs.SetString(option[0], option[1]); err != nil {
_ = fs.Close()
return err
}
}
if err = fs.SetFlag(OptionOverlayUserxattr); err != nil {
_ = fs.Close()
return err
}
if err = fs.Mount(target, 0); err != nil {
_ = fs.Close()
return err
}
return fs.Close()
}
func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
target := o.Target.String()
if !o.noPrefix {
@@ -194,7 +221,7 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
}
}
options := make([]string, 0, 4)
options := make([][2]string, 0, 2+len(o.lower))
if o.upper == zeroString && o.work == zeroString { // readonly
if len(o.Lower) < 2 {
@@ -205,15 +232,16 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
if len(o.Lower) == 0 {
return &OverlayArgumentError{OverlayEmptyLower, zeroString}
}
options = append(options,
OptionOverlayUpperdir+"="+o.upper,
OptionOverlayWorkdir+"="+o.work)
options = append(options, [][2]string{
{OptionOverlayUpperdir, o.upper},
{OptionOverlayWorkdir, o.work},
}...)
}
for _, lower := range o.lower {
options = append(options, [2]string{OptionOverlayLowerdir + "+", lower})
}
options = append(options,
OptionOverlayLowerdir+"="+strings.Join(o.lower, check.SpecialOverlayPath),
OptionOverlayUserxattr)
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption))
return k.mountOverlay(target, options)
}
func (o *MountOverlayOp) late(*setupState, syscallDispatcher) error { return nil }
+33 -33
View File
@@ -97,13 +97,12 @@ func TestMountOverlayOp(t *testing.T) {
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil),
call("mount", stub.ExpectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" +
"upperdir=overlay.upper.32768," +
"workdir=overlay.work.32768," +
"lowerdir=" +
`/host/var/lib/planterette/base/debian\:f92c9052:` +
`/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` +
"userxattr"}, nil, nil),
call("mountOverlay", stub.ExpectArgs{"/sysroot", [][2]string{
{"upperdir", "overlay.upper.32768"},
{"workdir", "overlay.work.32768"},
{"lowerdir+", `/host/var/lib/planterette/base/debian:f92c9052`},
{"lowerdir+", `/host/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052`},
}}, nil, nil),
}, nil},
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
@@ -129,11 +128,10 @@ func TestMountOverlayOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/nix/store", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" +
"lowerdir=" +
"/host/mnt-root/nix/.ro-store:" +
"/host/mnt-root/nix/.ro-store0," +
"userxattr"}, nil, nil),
call("mountOverlay", stub.ExpectArgs{"/nix/store", [][2]string{
{"lowerdir+", "/host/mnt-root/nix/.ro-store"},
{"lowerdir+", "/host/mnt-root/nix/.ro-store0"},
}}, nil, nil),
}, nil},
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
@@ -147,11 +145,10 @@ func TestMountOverlayOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"lowerdir=" +
"/host/mnt-root/nix/.ro-store:" +
"/host/mnt-root/nix/.ro-store0," +
"userxattr"}, nil, nil),
call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
{"lowerdir+", "/host/mnt-root/nix/.ro-store"},
{"lowerdir+", "/host/mnt-root/nix/.ro-store0"},
}}, nil, nil),
}, nil},
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
@@ -219,7 +216,11 @@ func TestMountOverlayOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, stub.UniqueError(0)),
call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
{"upperdir", "/host/mnt-root/nix/.rw-store/.upper"},
{"workdir", "/host/mnt-root/nix/.rw-store/.work"},
{"lowerdir+", "/host/mnt-root/nix/ro-store"},
}}, nil, stub.UniqueError(0)),
}, stub.UniqueError(0)},
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
@@ -233,11 +234,11 @@ func TestMountOverlayOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
"workdir=/host/mnt-root/nix/.rw-store/.work," +
"lowerdir=/host/mnt-root/nix/ro-store," +
"userxattr"}, nil, nil),
call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
{"upperdir", "/host/mnt-root/nix/.rw-store/.upper"},
{"workdir", "/host/mnt-root/nix/.rw-store/.work"},
{"lowerdir+", "/host/mnt-root/nix/ro-store"},
}}, nil, nil),
}, nil},
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
@@ -261,16 +262,15 @@ func TestMountOverlayOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
"workdir=/host/mnt-root/nix/.rw-store/.work," +
"lowerdir=" +
"/host/mnt-root/nix/ro-store:" +
"/host/mnt-root/nix/ro-store0:" +
"/host/mnt-root/nix/ro-store1:" +
"/host/mnt-root/nix/ro-store2:" +
"/host/mnt-root/nix/ro-store3," +
"userxattr"}, nil, nil),
call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
{"upperdir", "/host/mnt-root/nix/.rw-store/.upper"},
{"workdir", "/host/mnt-root/nix/.rw-store/.work"},
{"lowerdir+", "/host/mnt-root/nix/ro-store"},
{"lowerdir+", "/host/mnt-root/nix/ro-store0"},
{"lowerdir+", "/host/mnt-root/nix/ro-store1"},
{"lowerdir+", "/host/mnt-root/nix/ro-store2"},
{"lowerdir+", "/host/mnt-root/nix/ro-store3"},
}}, nil, nil),
}, nil},
})
+6
View File
@@ -40,6 +40,9 @@ const (
// SourceMqueue is used when mounting mqueue.
// Note that any source value is allowed when fstype is [FstypeMqueue].
SourceMqueue = "mqueue"
// SourceBinfmtMisc is used when mounting binfmt_misc.
// Note that any source value is allowed when fstype is [SourceBinfmtMisc].
SourceBinfmtMisc = "binfmt_misc"
// SourceOverlay is used when mounting overlay.
// Note that any source value is allowed when fstype is [FstypeOverlay].
SourceOverlay = "overlay"
@@ -70,6 +73,9 @@ const (
// FstypeMqueue represents the mqueue pseudo-filesystem.
// This filesystem type is usually mounted on /dev/mqueue.
FstypeMqueue = "mqueue"
// FstypeBinfmtMisc represents the binfmt_misc pseudo-filesystem.
// This filesystem type is usually mounted on /proc/sys/fs/binfmt_misc.
FstypeBinfmtMisc = "binfmt_misc"
// FstypeOverlay represents the overlay pseudo-filesystem.
// This filesystem type can be mounted anywhere in the container filesystem.
FstypeOverlay = "overlay"
-4
View File
@@ -10,7 +10,6 @@ import (
"testing"
"unsafe"
"hakurei.app/check"
"hakurei.app/vfs"
)
@@ -50,9 +49,6 @@ func TestToHost(t *testing.T) {
}
}
// InternalToHostOvlEscape exports toHost passed to [check.EscapeOverlayDataSegment].
func InternalToHostOvlEscape(s string) string { return check.EscapeOverlayDataSegment(toHost(s)) }
func TestCreateFile(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) {
t.Run("mkdir", func(t *testing.T) {
+1 -1
View File
@@ -39,7 +39,7 @@ func TestSyscall(t *testing.T) {
t.Errorf("Unmarshal: %v, want %v", got, tc.want)
}
})
if errors.As(tc.err, new(ext.SyscallNameError)) {
if _, ok := errors.AsType[ext.SyscallNameError](tc.err); ok {
return
}
+267
View File
@@ -0,0 +1,267 @@
package ext
import (
"os"
"runtime"
"syscall"
"unsafe"
)
// include/uapi/linux/mount.h
/*
* move_mount() flags.
*/
const (
MOVE_MOUNT_F_SYMLINKS = 1 << iota /* Follow symlinks on from path */
MOVE_MOUNT_F_AUTOMOUNTS /* Follow automounts on from path */
MOVE_MOUNT_F_EMPTY_PATH /* Empty from path permitted */
_
MOVE_MOUNT_T_SYMLINKS /* Follow symlinks on to path */
MOVE_MOUNT_T_AUTOMOUNTS /* Follow automounts on to path */
MOVE_MOUNT_T_EMPTY_PATH /* Empty to path permitted */
_
MOVE_MOUNT_SET_GROUP /* Set sharing group instead */
MOVE_MOUNT_BENEATH /* Mount beneath top mount */
)
/*
* fsopen() flags.
*/
const (
FSOPEN_CLOEXEC = 1 << iota
)
/*
* fspick() flags.
*/
const (
FSPICK_CLOEXEC = 1 << iota
FSPICK_SYMLINK_NOFOLLOW
FSPICK_NO_AUTOMOUNT
FSPICK_EMPTY_PATH
)
/*
* The type of fsconfig() call made.
*/
const (
FSCONFIG_SET_FLAG = iota /* Set parameter, supplying no value */
FSCONFIG_SET_STRING /* Set parameter, supplying a string value */
FSCONFIG_SET_BINARY /* Set parameter, supplying a binary blob value */
FSCONFIG_SET_PATH /* Set parameter, supplying an object by path */
FSCONFIG_SET_PATH_EMPTY /* Set parameter, supplying an object by (empty) path */
FSCONFIG_SET_FD /* Set parameter, supplying an object by fd */
FSCONFIG_CMD_CREATE /* Create new or reuse existing superblock */
FSCONFIG_CMD_RECONFIGURE /* Invoke superblock reconfiguration */
FSCONFIG_CMD_CREATE_EXCL /* Create new superblock, fail if reusing existing superblock */
)
/*
* fsmount() flags.
*/
const (
FSMOUNT_CLOEXEC = 1 << iota
)
/*
* Mount attributes.
*/
const (
MOUNT_ATTR_RDONLY = 0x00000001 /* Mount read-only */
MOUNT_ATTR_NOSUID = 0x00000002 /* Ignore suid and sgid bits */
MOUNT_ATTR_NODEV = 0x00000004 /* Disallow access to device special files */
MOUNT_ATTR_NOEXEC = 0x00000008 /* Disallow program execution */
MOUNT_ATTR__ATIME = 0x00000070 /* Setting on how atime should be updated */
MOUNT_ATTR_RELATIME = 0x00000000 /* - Update atime relative to mtime/ctime. */
MOUNT_ATTR_NOATIME = 0x00000010 /* - Do not update access times. */
MOUNT_ATTR_STRICTATIME = 0x00000020 /* - Always perform atime updates */
MOUNT_ATTR_NODIRATIME = 0x00000080 /* Do not update directory access times */
MOUNT_ATTR_IDMAP = 0x00100000 /* Idmap mount to @userns_fd in struct mount_attr. */
MOUNT_ATTR_NOSYMFOLLOW = 0x00200000 /* Do not follow symlinks */
)
// FS provides low-level wrappers around the suite of file-descriptor-based
// mount facilities in Linux.
type FS struct {
fd uintptr
c runtime.Cleanup
}
// newFS allocates a new [FS] for the specified fd.
func newFS(fd uintptr) *FS {
fs := FS{fd: fd}
fs.c = runtime.AddCleanup(&fs, func(fd uintptr) {
_ = syscall.Close(int(fd))
}, fd)
return &fs
}
// Close closes the underlying filesystem context.
func (fs *FS) Close() error {
if fs == nil {
return syscall.EINVAL
}
err := syscall.Close(int(fs.fd))
fs.c.Stop()
return err
}
// OpenFS creates a new filesystem context.
func OpenFS(fsname string, flags int) (fs *FS, err error) {
var s *byte
s, err = syscall.BytePtrFromString(fsname)
if err != nil {
return
}
fd, _, errno := syscall.Syscall(
SYS_FSOPEN,
uintptr(unsafe.Pointer(s)),
uintptr(flags|FSOPEN_CLOEXEC),
0,
)
if errno != 0 {
err = os.NewSyscallError("fsopen", errno)
} else {
fs = newFS(fd)
}
return
}
// PickFS selects filesystem for reconfiguration.
func PickFS(dirfd int, pathname string, flags int) (fs *FS, err error) {
var s *byte
s, err = syscall.BytePtrFromString(pathname)
if err != nil {
return
}
fd, _, errno := syscall.Syscall(
SYS_FSPICK,
uintptr(dirfd),
uintptr(unsafe.Pointer(s)),
uintptr(flags|FSPICK_CLOEXEC),
)
if errno != 0 {
err = os.NewSyscallError("fspick", errno)
} else {
fs = newFS(fd)
}
return
}
// config configures new or existing filesystem context.
func (fs *FS) config(cmd uint, key *byte, value unsafe.Pointer, aux int) (err error) {
_, _, errno := syscall.Syscall6(
SYS_FSCONFIG,
fs.fd,
uintptr(cmd),
uintptr(unsafe.Pointer(key)),
uintptr(value),
uintptr(aux),
0,
)
if errno != 0 {
err = os.NewSyscallError("fsconfig", errno)
}
return
}
// SetFlag sets the flag parameter named by key. ([FSCONFIG_SET_FLAG])
func (fs *FS) SetFlag(key string) (err error) {
var s *byte
s, err = syscall.BytePtrFromString(key)
if err != nil {
return
}
return fs.config(FSCONFIG_SET_FLAG, s, nil, 0)
}
// SetString sets the string parameter named by key to the value specified by
// value. ([FSCONFIG_SET_STRING])
func (fs *FS) SetString(key, value string) (err error) {
var s0 *byte
s0, err = syscall.BytePtrFromString(key)
if err != nil {
return
}
var s1 *byte
s1, err = syscall.BytePtrFromString(value)
if err != nil {
return
}
return fs.config(FSCONFIG_SET_STRING, s0, unsafe.Pointer(s1), 0)
}
// mount instantiates mount object from filesystem context.
func (fs *FS) mount(flags, attrFlags int) (fsfd int, err error) {
r, _, errno := syscall.Syscall(
SYS_FSMOUNT,
fs.fd,
uintptr(flags|FSMOUNT_CLOEXEC),
uintptr(attrFlags),
)
fsfd = int(r)
if errno != 0 {
err = os.NewSyscallError("fsmount", errno)
}
return
}
// MoveMount moves or attaches mount object to filesystem.
func MoveMount(
fromDirfd int,
fromPathname string,
toDirfd int,
toPathname string,
flags int,
) (err error) {
var s0 *byte
s0, err = syscall.BytePtrFromString(fromPathname)
if err != nil {
return
}
var s1 *byte
s1, err = syscall.BytePtrFromString(toPathname)
if err != nil {
return
}
_, _, errno := syscall.Syscall6(
SYS_MOVE_MOUNT,
uintptr(fromDirfd),
uintptr(unsafe.Pointer(s0)),
uintptr(toDirfd),
uintptr(unsafe.Pointer(s1)),
uintptr(flags),
0,
)
if errno != 0 {
err = os.NewSyscallError("move_mount", errno)
}
return
}
// Mount attaches the underlying filesystem context to the specified pathname.
func (fs *FS) Mount(pathname string, attrFlags int) error {
if err := fs.config(FSCONFIG_CMD_CREATE_EXCL, nil, nil, 0); err != nil {
return err
}
fd, err := fs.mount(0, attrFlags)
if err != nil {
return err
}
err = MoveMount(
fd, "",
-1, pathname,
MOVE_MOUNT_F_EMPTY_PATH,
)
closeErr := syscall.Close(fd)
if err == nil {
err = closeErr
}
return err
}
+2
View File
@@ -42,6 +42,8 @@ var (
AbsDevShm = unsafeAbs(DevShm)
// AbsProc is [Proc] as [check.Absolute].
AbsProc = unsafeAbs(Proc)
// AbsProcSys is [ProcSys] as [check.Absolute].
AbsProcSys = unsafeAbs(ProcSys)
// AbsProcSelfExe is [ProcSelfExe] as [check.Absolute].
AbsProcSelfExe = unsafeAbs(ProcSelfExe)
// AbsSys is [Sys] as [check.Absolute].
Generated
+8 -8
View File
@@ -7,32 +7,32 @@
]
},
"locked": {
"lastModified": 1772985280,
"narHash": "sha256-FdrNykOoY9VStevU4zjSUdvsL9SzJTcXt4omdEDZDLk=",
"lastModified": 1780361225,
"narHash": "sha256-wnV9ttf4fPWNonBIQmvlrSlNpQYgx5HgWWd007mwIFA=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "8f736f007139d7f70752657dff6a401a585d6cbc",
"rev": "e28654b71096e08c019d4861ca26acb646f583d8",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-25.11",
"ref": "release-26.05",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1772822230,
"narHash": "sha256-yf3iYLGbGVlIthlQIk5/4/EQDZNNEmuqKZkQssMljuw=",
"lastModified": 1780453794,
"narHash": "sha256-bXMRa9VTsHSPXL4Cw8R6JJLQeY3Y/IP4+YJCYVmQ7FY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "71caefce12ba78d84fe618cf61644dce01cf3a96",
"rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"ref": "nixos-26.05",
"repo": "nixpkgs",
"type": "github"
}
+4 -5
View File
@@ -2,10 +2,10 @@
description = "hakurei container tool and nixos module";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
home-manager = {
url = "github:nix-community/home-manager/release-25.11";
url = "github:nix-community/home-manager/release-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@@ -37,7 +37,7 @@
inherit (pkgs)
runCommandLocal
callPackage
nixfmt-rfc-style
nixfmt
deadnix
statix
;
@@ -57,7 +57,7 @@
sharefs = callPackage ./cmd/sharefs/test { inherit system self; };
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt ]; } ''
cd ${./.}
echo "running nixfmt..."
@@ -139,7 +139,6 @@
GOCACHE="$(mktemp -d)" \
PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \
DESTDIR="$out" \
HAKUREI_VERSION="v${hakurei.version}" \
./all.sh
'';
}
+7 -6
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"reflect"
"strings"
"hakurei.app/check"
)
@@ -78,17 +79,17 @@ type FSImplError struct{ Value FilesystemConfig }
func (f FSImplError) Error() string {
implType := reflect.TypeOf(f.Value)
var name string
for implType != nil && implType.Kind() == reflect.Ptr {
name += "*"
var buf strings.Builder
for implType != nil && implType.Kind() == reflect.Pointer {
buf.WriteByte('*')
implType = implType.Elem()
}
if implType != nil {
name += implType.Name()
buf.WriteString(implType.Name())
} else {
name += "nil"
buf.WriteString("nil")
}
return fmt.Sprintf("implementation %s not supported", name)
return "implementation " + buf.String() + " not supported"
}
// FilesystemConfigJSON is the [json] adapter for [FilesystemConfig].
+2 -2
View File
@@ -103,7 +103,7 @@ func TestFilesystemConfigJSON(t *testing.T) {
t.Run("marshal", func(t *testing.T) {
t.Parallel()
wantErr := tc.wantErr
if errors.As(wantErr, new(hst.FSTypeError)) {
if _, ok := errors.AsType[hst.FSTypeError](wantErr); ok {
// for unsupported implementation tc
wantErr = hst.FSImplError{Value: stubFS{"cat"}}
}
@@ -139,7 +139,7 @@ func TestFilesystemConfigJSON(t *testing.T) {
t.Run("unmarshal", func(t *testing.T) {
t.Parallel()
if tc.data == "\x00" && tc.sData == "\x00" {
if errors.As(tc.wantErr, new(hst.FSImplError)) {
if _, ok := errors.AsType[hst.FSImplError](tc.wantErr); ok {
// this error is only returned on marshal
return
}
+1 -6
View File
@@ -43,18 +43,13 @@ func (e *FSEphemeral) Apply(z *ApplyState) {
return
}
size := e.Size
if size < 0 {
size = 0
}
perm := e.Perm
if perm == 0 {
perm = fsEphemeralDefaultPerm
}
if e.Write {
z.Tmpfs(e.Target, size, perm)
z.Tmpfs(e.Target, max(e.Size, 0), perm)
} else {
z.Readonly(e.Target, perm)
}
+2 -2
View File
@@ -80,7 +80,7 @@ func unescapeValue(v []byte) (val []byte, errno ParseError) {
continue
}
if ib := bytes.IndexByte([]byte("-_/.\\*"), b); ib != -1 { // - // _/.\*
if found := bytes.Contains([]byte("-_/.\\*"), []byte{b}); found { // - // _/.\*
goto opt
} else if b >= '0' && b <= '9' { // 0-9
goto opt
@@ -101,7 +101,7 @@ func unescapeValue(v []byte) (val []byte, errno ParseError) {
break
}
if c, err := hex.Decode(val[i:i+1], v[iu+1:iu+3]); err != nil {
if errors.As(err, new(hex.InvalidByteError)) {
if _, ok := errors.AsType[hex.InvalidByteError](err); ok {
errno = ErrBadValHexByte
break
}
+17
View File
@@ -2,6 +2,7 @@ package kobject
import (
"errors"
"maps"
"strconv"
"strings"
"unsafe"
@@ -28,6 +29,22 @@ type Event struct {
Subsystem string `json:"subsystem"`
}
// Clone returns a copy of e.
func (e *Event) Clone() Event {
v := *e
v.Env = maps.Clone(e.Env)
return v
}
// makeColdboot allocates a new [Object] from e in [StateColdboot].
func (e *Event) makeColdboot() *Object {
return &Object{
State: StateColdboot,
DevPath: e.DevPath,
Subsystem: e.Subsystem,
}
}
// Populate populates e with the contents of a [uevent.Message].
//
// The ACTION and DEVPATH environment variables are ignored and assumed to be
+491
View File
@@ -0,0 +1,491 @@
// Package kobject interprets uevent messages from a NETLINK_KOBJECT_UEVENT socket.
package kobject
import (
"context"
"fmt"
"maps"
"slices"
"strconv"
"sync"
"hakurei.app/internal/report"
"hakurei.app/internal/uevent"
)
const (
// StateColdboot denotes an [Object] populated by a coldboot event. It is
// eligible for all event actions.
StateColdboot = iota
// StateNew denotes an [Object] previously populated by a [uevent.KOBJ_ADD]
// event, but has not yet been targeted by a [uevent.KOBJ_BIND] event, or
// has been targeted by a [uevent.KOBJ_UNBIND] event.
StateNew
// StateBound denotes an [Object] that has been targeted by a
// [uevent.KOBJ_BIND] and has not been targeted by a [uevent.KOBJ_UNBIND]
// after that.
StateBound
)
// Object represents a kernel object.
type Object struct {
// Origin of the object.
State int `json:"state,omitempty"`
// Set by [uevent.KOBJ_OFFLINE] and [uevent.KOBJ_ONLINE].
Offline bool `json:"offline,omitempty"`
// alloc_uevent_skb: devpath
DevPath string `json:"devpath"`
// registered per-driver (optional)
ModAlias string `json:"modalias,omitempty"`
// dev_driver_uevent: drv->name (optional)
Driver string `json:"driver,omitempty"`
// SUBSYSTEM value set by the kernel.
Subsystem string `json:"subsystem"`
// Uninterpreted environment variable pairs. An entry missing a separator
// gains the value "\x00".
Env map[string]string `json:"env"`
}
// Clone returns the address of a copy of o.
func (o *Object) Clone() *Object {
v := *o
v.Env = maps.Clone(o.Env)
return &v
}
// GoString returns compound literal for the underlying value.
func (o *Object) GoString() string {
return fmt.Sprintf("&%#v", *o)
}
// merge merges uninterpreted environment variable pairs from an [Event].
func (o *Object) merge(env map[string]string) {
for k, v := range env {
if v == "\x00" {
continue
}
switch k {
case "MODALIAS":
o.ModAlias = v
continue
case "DRIVER":
o.Driver = v
continue
default:
if o.Env == nil {
o.Env = make(map[string]string)
}
o.Env[k] = v
}
}
}
// update updates o with pairs from env, optionally stripping visited pairs.
func (o *Object) update(env map[string]string, strip bool) {
for k := range o.Env {
if v, ok := env[k]; ok {
if strip {
delete(env, k)
}
o.Env[k] = v
}
}
}
// A pendingIterator is a callback currently iterating through objects targeted
// by ongoing events.
type pendingIterator struct {
f func(o *Object, act uevent.KobjectAction) bool
done chan<- struct{}
}
// State processes a stream of [Event] populated from [uevent.Message] received
// from a NETLINK_KOBJECT_UEVENT socket and presents an efficient representation
// of kernel state.
type State struct {
// Next expected SEQNUM.
seq uint64
// DevPath to environment variables.
uevent map[string]*Object
// Synchronises access to uevent and its objects.
ueventMu sync.RWMutex
// Alive iterators.
iter []*pendingIterator
// Synchronises access to iter.
iterMu sync.Mutex
// UUID for synthetic [uevent.Coldboot] events.
coldboot uevent.UUID
// Called on [uevent.KOBJ_CHANGE] with stripped environment variables.
handleChange func(o *Object, env map[string]string)
// Reports errors populating [Event] from [uevent.Message]. A user-supplied
// nil value is replaced with a noop.
reportErr func(error)
}
// New returns the address of a new [State].
func New(
coldboot uevent.UUID,
handleChange func(o *Object, env map[string]string),
reportErr func(error),
) *State {
return &State{
uevent: make(map[string]*Object),
coldboot: coldboot,
handleChange: handleChange,
reportErr: reportErr,
}
}
// deleteIter removes an iterator from s. Must be called after acquiring iterMu.
func (s *State) deleteIter(p *pendingIterator) {
s.iter = slices.DeleteFunc(s.iter, func(v *pendingIterator) bool {
return p == v
})
}
// dispatchIter broadcasts an [Object] to all alive iterators.
func (s *State) dispatchIter(o *Object, act uevent.KobjectAction) {
s.iterMu.Lock()
defer s.iterMu.Unlock()
for _, p := range s.iter {
if !p.f(o, act) {
s.deleteIter(p)
close(p.done)
}
}
}
// Range calls f on all current and upcoming [Object] values tracked by s until
// f returns false or the context is cancelled. f must not retain o or modify
// the value it points to.
func (s *State) Range(
ctx context.Context,
f func(o *Object, act uevent.KobjectAction) bool,
) {
done := make(chan struct{})
p := pendingIterator{f, done}
s.iterMu.Lock()
s.ueventMu.RLock()
for _, o := range s.uevent {
if !f(o, uevent.KOBJ_ADD) {
s.ueventMu.RUnlock()
s.iterMu.Unlock()
return
}
}
s.ueventMu.RUnlock()
s.iter = append(s.iter, &p)
s.iterMu.Unlock()
select {
case <-ctx.Done():
s.iterMu.Lock()
s.deleteIter(&p)
s.iterMu.Unlock()
return
case <-done:
// deregistered by dispatchIter
return
}
}
// An EventError describes a malformed or inconsistent [Event].
type EventError struct {
Kind int `json:"fault"`
E Event `json:"event"`
O *Object `json:"object,omitempty"`
}
var _ report.RepresentableError = EventError{}
func (EventError) Representable() {}
const (
// EUnexpectedColdboot is reported for a coldboot event with action other
// than the expected [uevent.KOBJ_ADD].
EUnexpectedColdboot = iota
// EDuplicateAdd is reported for a [uevent.KOBJ_ADD] event on a
// still-existing entry that was not the result of a coldboot.
EDuplicateAdd
// EBadTarget is reported for an event on a nonexistent [Object]. This is
// generally only possible before coldboot completes.
EBadTarget
// ERemoveState is reported for a [uevent.KOBJ_REMOVE] event targeting an
// entry in a state other than [StateColdboot] and [StateNew].
ERemoveState
// EUnexpectedOffline is reported for a [uevent.KOBJ_OFFLINE] or
// [uevent.KOBJ_ONLINE] event targeting an already offline or online object.
EUnexpectedOffline
// EBindState is reported for a [uevent.KOBJ_BIND] event targeting an entry
// in a state other than [StateColdboot] and [StateNew].
EBindState
// EUnbindState is reported for a [uevent.KOBJ_UNBIND] event targeting an
// entry in a state other than [StateBound].
EUnbindState
// EMalformedMove is reported for a [uevent.KOBJ_MOVE] event missing the
// DEVPATH_OLD environment variable.
EMalformedMove
)
func (e EventError) Error() string {
switch e.Kind {
case EUnexpectedColdboot:
return "unexpected " + e.E.Action.String() + " coldboot event"
case EDuplicateAdd:
return "duplicate add event on devpath " + strconv.Quote(e.E.DevPath)
case EBadTarget:
return "unexpected " + e.E.Action.String() + " event on devpath " +
strconv.Quote(e.E.DevPath)
case ERemoveState:
if e.O == nil {
return "invalid remove event error"
}
return "remove event targeting devpath " + strconv.Quote(e.E.DevPath) +
" in state " + strconv.Itoa(e.O.State)
case EUnexpectedOffline:
if e.O == nil {
return "invalid unexpected offline error"
}
if e.O.Offline {
return "offline event targeting devpath " + strconv.Quote(e.E.DevPath)
}
return "online event targeting devpath " + strconv.Quote(e.E.DevPath)
case EBindState:
if e.O == nil {
return "invalid bind state error"
}
return "bind event targeting devpath " + strconv.Quote(e.E.DevPath) +
" in state " + strconv.Itoa(e.O.State)
case EUnbindState:
if e.O == nil {
return "invalid unbind state error"
}
return "unbind event targeting devpath " + strconv.Quote(e.E.DevPath) +
" in state " + strconv.Itoa(e.O.State)
case EMalformedMove:
return "move event targeting devpath " + strconv.Quote(e.E.DevPath) +
" missing DEVPATH_OLD"
default:
return "invalid event error kind " + strconv.Itoa(e.Kind)
}
}
// NewError returns a new [EventError] for e and o.
func (e *Event) NewError(kind int, o *Object) error {
if o != nil {
o = o.Clone()
}
return EventError{kind, e.Clone(), o}
}
// processEvent merges an event into s.
func (s *State) processEvent(e *Event) {
s.ueventMu.Lock()
defer s.ueventMu.Unlock()
coldboot := e.Synth != nil
if e.Action != uevent.KOBJ_ADD && coldboot {
s.reportErr(e.NewError(EUnexpectedColdboot, nil))
return
}
switch act := e.Action; act {
case uevent.KOBJ_ADD:
if e.Synth == nil {
if o, ok := s.uevent[e.DevPath]; ok {
s.reportErr(e.NewError(EDuplicateAdd, o))
o.merge(e.Env)
s.dispatchIter(o, act)
return
}
}
o := e.makeColdboot()
if !coldboot {
o.State = StateNew
}
o.merge(e.Env)
s.uevent[e.DevPath] = o
s.dispatchIter(o, act)
return
case uevent.KOBJ_REMOVE:
if o, ok := s.uevent[e.DevPath]; !ok {
s.reportErr(e.NewError(EBadTarget, nil))
return
} else if o.State != StateColdboot && o.State != StateNew {
s.reportErr(e.NewError(ERemoveState, o))
}
delete(s.uevent, e.DevPath)
return
case uevent.KOBJ_CHANGE:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(e.NewError(EBadTarget, nil))
// this suffers from the coldboot race window similar to KOBJ_MOVE,
// however this action combines driver-specific and change-specific
// environment variables and combines them with environment
// variables meant to convey state of the kobject, and it is not
// possible to reliably separate them, so this fallback avoids the
// race at the cost of including some garbage in tracked state
o = e.makeColdboot()
o.merge(e.Env)
s.uevent[e.DevPath] = o
s.dispatchIter(o, act)
return
}
o.update(e.Env, true)
if s.handleChange != nil {
s.handleChange(o, e.Env)
}
s.dispatchIter(o, act)
return
case uevent.KOBJ_MOVE:
var o *Object
if old, ok := e.Env["DEVPATH_OLD"]; !ok {
s.reportErr(e.NewError(EMalformedMove, nil))
// not reached
o = e.makeColdboot()
} else if o, ok = s.uevent[old]; !ok {
s.reportErr(e.NewError(EBadTarget, nil))
// this generally happens during coldboot, dropping the event here
// may cause inconsistent state if the coldboot event for this
// object was generated before the bind event
delete(e.Env, "DEVPATH_OLD")
o = e.makeColdboot()
} else {
delete(s.uevent, old)
delete(e.Env, "DEVPATH_OLD")
}
o.merge(e.Env)
s.uevent[e.DevPath] = o
o.DevPath = e.DevPath
s.dispatchIter(o, act)
return
case uevent.KOBJ_ONLINE:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(e.NewError(EBadTarget, nil))
// coldboot race window similar to an unexpected KOBJ_MOVE
o = e.makeColdboot()
s.uevent[e.DevPath] = o
o.merge(e.Env)
}
if !o.Offline {
s.reportErr(e.NewError(EUnexpectedOffline, o))
}
o.Offline = false
s.dispatchIter(o, act)
return
case uevent.KOBJ_OFFLINE:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(e.NewError(EBadTarget, nil))
// coldboot race window similar to an unexpected KOBJ_MOVE
o = e.makeColdboot()
s.uevent[e.DevPath] = o
o.merge(e.Env)
}
if o.Offline {
s.reportErr(e.NewError(EUnexpectedOffline, o))
}
o.Offline = true
s.dispatchIter(o, act)
return
case uevent.KOBJ_BIND:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(e.NewError(EBadTarget, nil))
// coldboot race window similar to an unexpected KOBJ_MOVE
o = e.makeColdboot()
s.uevent[e.DevPath] = o
}
if o.State != StateColdboot && o.State != StateNew {
s.reportErr(e.NewError(EBindState, o))
}
o.State = StateBound
o.merge(e.Env)
s.dispatchIter(o, act)
return
case uevent.KOBJ_UNBIND:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(e.NewError(EBadTarget, nil))
// coldboot race window similar to an unexpected KOBJ_MOVE, but does
// not result in inconsistent state if dropped
return
}
if o.State != StateBound {
s.reportErr(e.NewError(EUnbindState, o))
}
o.State = StateNew
o.Driver = ""
s.dispatchIter(o, act)
return
default: // not reached
s.reportErr(fmt.Errorf("invalid action %d", e.Action))
return
}
}
// BadSequenceError is reported for an unexpected SEQNUM.
type BadSequenceError struct{ Got, Want uint64 }
func (e BadSequenceError) Error() string {
return "SEQNUM=" + strconv.FormatUint(e.Got, 10) +
", want " + strconv.FormatUint(e.Want, 10)
}
// Consume receives uevent messages and updates s to reflect state of kernel.
func (s *State) Consume(ctx context.Context, events <-chan *uevent.Message) {
if s.uevent == nil {
s.uevent = make(map[string]*Object)
}
if s.reportErr == nil {
s.reportErr = func(error) {}
}
var e Event
for {
select {
case <-ctx.Done():
return
case m, ok := <-events:
if !ok {
return
}
e.Populate(s.reportErr, m)
// skip external synthetic event
if e.Synth != nil && *e.Synth != s.coldboot {
continue
}
if s.seq == 0 {
s.seq = e.Sequence
}
if s.seq != e.Sequence {
s.reportErr(BadSequenceError{e.Sequence, s.seq})
}
s.seq++
s.processEvent(&e)
}
}
}
File diff suppressed because it is too large Load Diff
+266
View File
@@ -0,0 +1,266 @@
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXPWRBN:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXPWRBN:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:LNXPWRBN:","SEQNUM=777"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXPWRBN:00/wakeup/wakeup7","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXPWRBN:00/wakeup/wakeup7","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=778"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0010:00/LNXCPU:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0010:00/LNXCPU:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:LNXCPU:","SEQNUM=779"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0010:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0010:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:ACPI0010:PNP0A05:","SEQNUM=780"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0103:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0103:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0103:","SEQNUM=781"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/PNP0A06:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/PNP0A06:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0A06:","SEQNUM=782"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/PNP0A06:01","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/PNP0A06:01","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0A06:","SEQNUM=783"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/PNP0A06:02","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/PNP0A06:02","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0A06:","SEQNUM=784"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/QEMU0002:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/QEMU0002:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:QEMU0002:","SEQNUM=785"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=786"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:00/wakeup/wakeup0","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:00/wakeup/wakeup0","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=787"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0303:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0303:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0303:","SEQNUM=788"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0400:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0400:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0400:","SEQNUM=789"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0501:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0501:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0501:","SEQNUM=790"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0700:00/device:02","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0700:00/device:02","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=791"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0700:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0700:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0700:","SEQNUM=792"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0B00:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0B00:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0B00:","SEQNUM=793"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0F13:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0F13:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0F13:","SEQNUM=794"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=795"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/wakeup/wakeup1","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/wakeup/wakeup1","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=796"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:03","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:03","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=797"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:03/wakeup/wakeup2","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:03/wakeup/wakeup2","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=798"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:04","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:04","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=799"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:04/wakeup/wakeup3","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:04/wakeup/wakeup3","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=800"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:05","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:05","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=801"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:05/wakeup/wakeup4","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:05/wakeup/wakeup4","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=802"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:06","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:06","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=803"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:06/wakeup/wakeup5","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:06/wakeup/wakeup5","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=804"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=805"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=806"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:09","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:09","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=807"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0a","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0a","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=808"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0b","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0b","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=809"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0c","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0c","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=810"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0d","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0d","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=811"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0e","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0e","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=812"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0f","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0f","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=813"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:10","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:10","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=814"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:11","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:11","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=815"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:12","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:12","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=816"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:13","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:13","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=817"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:14","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:14","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=818"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:15","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:15","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=819"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:16","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:16","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=820"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:17","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:17","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=821"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:18","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:18","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=822"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:19","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:19","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=823"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1a","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1a","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=824"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1b","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1b","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=825"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1c","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1c","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=826"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1d","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1d","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=827"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1e","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1e","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=828"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1f","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1f","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=829"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:20","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:20","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=830"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:21","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:21","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=831"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:22","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:22","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=832"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0A03:","SEQNUM=833"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/wakeup/wakeup6","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/wakeup/wakeup6","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=834"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0C0F:","SEQNUM=835"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:01","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:01","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0C0F:","SEQNUM=836"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:02","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:02","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0C0F:","SEQNUM=837"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:03","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:03","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0C0F:","SEQNUM=838"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:04","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:04","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0C0F:","SEQNUM=839"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:LNXSYBUS:","SEQNUM=840"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:01","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:01","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:LNXSYBUS:","SEQNUM=841"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:LNXSYSTM:","SEQNUM=842"]}
{"action":"add","devpath":"/devices/breakpoint","env":["ACTION=add","DEVPATH=/devices/breakpoint","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=843"]}
{"action":"add","devpath":"/devices/cpu","env":["ACTION=add","DEVPATH=/devices/cpu","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=844"]}
{"action":"add","devpath":"/devices/kprobe","env":["ACTION=add","DEVPATH=/devices/kprobe","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=845"]}
{"action":"add","devpath":"/devices/msr","env":["ACTION=add","DEVPATH=/devices/msr","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=846"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:00.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:00.0","SUBSYSTEM=pci","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","PCI_CLASS=60000","PCI_ID=8086:1237","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:00.0","MODALIAS=pci:v00008086d00001237sv00001AF4sd00001100bc06sc00i00","SEQNUM=847"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.0","SUBSYSTEM=pci","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","PCI_CLASS=60100","PCI_ID=8086:7000","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:01.0","MODALIAS=pci:v00008086d00007000sv00001AF4sd00001100bc06sc01i00","SEQNUM=848"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata1/ata_port/ata1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata1/ata_port/ata1","SUBSYSTEM=ata_port","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=849"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata1/host0/scsi_host/host0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata1/host0/scsi_host/host0","SUBSYSTEM=scsi_host","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=850"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata1/host0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata1/host0","SUBSYSTEM=scsi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=scsi_host","SEQNUM=851"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata1/link1/ata_link/link1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata1/link1/ata_link/link1","SUBSYSTEM=ata_link","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=852"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata1/link1/dev1.0/ata_device/dev1.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata1/link1/dev1.0/ata_device/dev1.0","SUBSYSTEM=ata_device","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=853"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata1/link1/dev1.1/ata_device/dev1.1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata1/link1/dev1.1/ata_device/dev1.1","SUBSYSTEM=ata_device","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=854"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/ata_port/ata2","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/ata_port/ata2","SUBSYSTEM=ata_port","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=855"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/host1/scsi_host/host1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/host1/scsi_host/host1","SUBSYSTEM=scsi_host","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=856"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0/bsg/1:0:0:0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0/bsg/1:0:0:0","SUBSYSTEM=bsg","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=251","MINOR=0","DEVNAME=bsg/1:0:0:0","SEQNUM=857"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0/scsi_device/1:0:0:0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0/scsi_device/1:0:0:0","SUBSYSTEM=scsi_device","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=858"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0","SUBSYSTEM=scsi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=scsi_device","MODALIAS=scsi:t-0x05","SEQNUM=859"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0","SUBSYSTEM=scsi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=scsi_target","SEQNUM=860"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/host1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/host1","SUBSYSTEM=scsi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=scsi_host","SEQNUM=861"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/link2/ata_link/link2","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/link2/ata_link/link2","SUBSYSTEM=ata_link","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=862"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/link2/dev2.0/ata_device/dev2.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/link2/dev2.0/ata_device/dev2.0","SUBSYSTEM=ata_device","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=863"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/link2/dev2.1/ata_device/dev2.1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/link2/dev2.1/ata_device/dev2.1","SUBSYSTEM=ata_device","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=864"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1","SUBSYSTEM=pci","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=ata_piix","PCI_CLASS=10180","PCI_ID=8086:7010","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:01.1","MODALIAS=pci:v00008086d00007010sv00001AF4sd00001100bc01sc01i80","SEQNUM=865"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.3","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.3","SUBSYSTEM=pci","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","PCI_CLASS=68000","PCI_ID=8086:7113","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:01.3","MODALIAS=pci:v00008086d00007113sv00001AF4sd00001100bc06sc80i00","SEQNUM=866"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:02.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:02.0","SUBSYSTEM=pci","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","PCI_CLASS=20000","PCI_ID=8086:100E","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:02.0","MODALIAS=pci:v00008086d0000100Esv00001AF4sd00001100bc02sc00i00","SEQNUM=867"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:03.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:03.0","SUBSYSTEM=pci","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=virtio-pci","PCI_CLASS=10000","PCI_ID=1AF4:1001","PCI_SUBSYS_ID=1AF4:0002","PCI_SLOT_NAME=0000:00:03.0","MODALIAS=pci:v00001AF4d00001001sv00001AF4sd00000002bc01sc00i00","SEQNUM=868"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:03.0/virtio0/block/vda","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:03.0/virtio0/block/vda","SUBSYSTEM=block","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=254","MINOR=0","DEVNAME=vda","DEVTYPE=disk","DISKSEQ=1","SEQNUM=869"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:03.0/virtio0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:03.0/virtio0","SUBSYSTEM=virtio","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=virtio_blk","MODALIAS=virtio:d00000002v00001AF4","SEQNUM=870"]}
{"action":"add","devpath":"/devices/pci0000:00/QEMU0002:00","env":["ACTION=add","DEVPATH=/devices/pci0000:00/QEMU0002:00","SUBSYSTEM=platform","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:QEMU0002:","SEQNUM=871"]}
{"action":"add","devpath":"/devices/pci0000:00/pci_bus/0000:00","env":["ACTION=add","DEVPATH=/devices/pci0000:00/pci_bus/0000:00","SUBSYSTEM=pci_bus","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=872"]}
{"action":"add","devpath":"/devices/platform/PNP0103:00","env":["ACTION=add","DEVPATH=/devices/platform/PNP0103:00","SUBSYSTEM=platform","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0103:","SEQNUM=873"]}
{"action":"add","devpath":"/devices/platform/pcspkr","env":["ACTION=add","DEVPATH=/devices/platform/pcspkr","SUBSYSTEM=platform","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=platform:pcspkr","SEQNUM=874"]}
{"action":"add","devpath":"/devices/platform/reg-dummy/regulator/regulator.0","env":["ACTION=add","DEVPATH=/devices/platform/reg-dummy/regulator/regulator.0","SUBSYSTEM=regulator","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=875"]}
{"action":"add","devpath":"/devices/platform/reg-dummy","env":["ACTION=add","DEVPATH=/devices/platform/reg-dummy","SUBSYSTEM=platform","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=reg-dummy","MODALIAS=platform:reg-dummy","SEQNUM=876"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0/serial8250:0.1/tty/ttyS1","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0/serial8250:0.1/tty/ttyS1","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=65","DEVNAME=ttyS1","SEQNUM=877"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0/serial8250:0.1","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0/serial8250:0.1","SUBSYSTEM=serial-base","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=port","DRIVER=port","SEQNUM=878"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0/serial8250:0.2/tty/ttyS2","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0/serial8250:0.2/tty/ttyS2","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=66","DEVNAME=ttyS2","SEQNUM=879"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0/serial8250:0.2","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0/serial8250:0.2","SUBSYSTEM=serial-base","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=port","DRIVER=port","SEQNUM=880"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0/serial8250:0.3/tty/ttyS3","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0/serial8250:0.3/tty/ttyS3","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=67","DEVNAME=ttyS3","SEQNUM=881"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0/serial8250:0.3","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0/serial8250:0.3","SUBSYSTEM=serial-base","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=port","DRIVER=port","SEQNUM=882"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0","SUBSYSTEM=serial-base","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=ctrl","DRIVER=ctrl","SEQNUM=883"]}
{"action":"add","devpath":"/devices/platform/serial8250","env":["ACTION=add","DEVPATH=/devices/platform/serial8250","SUBSYSTEM=platform","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=serial8250","MODALIAS=platform:serial8250","SEQNUM=884"]}
{"action":"add","devpath":"/devices/pnp0/00:00","env":["ACTION=add","DEVPATH=/devices/pnp0/00:00","SUBSYSTEM=pnp","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=885"]}
{"action":"add","devpath":"/devices/pnp0/00:01","env":["ACTION=add","DEVPATH=/devices/pnp0/00:01","SUBSYSTEM=pnp","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=886"]}
{"action":"add","devpath":"/devices/pnp0/00:02","env":["ACTION=add","DEVPATH=/devices/pnp0/00:02","SUBSYSTEM=pnp","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=887"]}
{"action":"add","devpath":"/devices/pnp0/00:03","env":["ACTION=add","DEVPATH=/devices/pnp0/00:03","SUBSYSTEM=pnp","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=888"]}
{"action":"add","devpath":"/devices/pnp0/00:04/00:04:0/00:04:0.0/tty/ttyS0","env":["ACTION=add","DEVPATH=/devices/pnp0/00:04/00:04:0/00:04:0.0/tty/ttyS0","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=64","DEVNAME=ttyS0","SEQNUM=889"]}
{"action":"add","devpath":"/devices/pnp0/00:04/00:04:0/00:04:0.0","env":["ACTION=add","DEVPATH=/devices/pnp0/00:04/00:04:0/00:04:0.0","SUBSYSTEM=serial-base","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=port","DRIVER=port","SEQNUM=890"]}
{"action":"add","devpath":"/devices/pnp0/00:04/00:04:0","env":["ACTION=add","DEVPATH=/devices/pnp0/00:04/00:04:0","SUBSYSTEM=serial-base","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=ctrl","DRIVER=ctrl","SEQNUM=891"]}
{"action":"add","devpath":"/devices/pnp0/00:04","env":["ACTION=add","DEVPATH=/devices/pnp0/00:04","SUBSYSTEM=pnp","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=serial","SEQNUM=892"]}
{"action":"add","devpath":"/devices/pnp0/00:05","env":["ACTION=add","DEVPATH=/devices/pnp0/00:05","SUBSYSTEM=pnp","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=893"]}
{"action":"add","devpath":"/devices/software","env":["ACTION=add","DEVPATH=/devices/software","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=894"]}
{"action":"add","devpath":"/devices/system/clockevents/broadcast","env":["ACTION=add","DEVPATH=/devices/system/clockevents/broadcast","SUBSYSTEM=clockevents","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=895"]}
{"action":"add","devpath":"/devices/system/clockevents/clockevent0","env":["ACTION=add","DEVPATH=/devices/system/clockevents/clockevent0","SUBSYSTEM=clockevents","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=896"]}
{"action":"add","devpath":"/devices/system/clocksource/clocksource0","env":["ACTION=add","DEVPATH=/devices/system/clocksource/clocksource0","SUBSYSTEM=clocksource","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=897"]}
{"action":"add","devpath":"/devices/system/container/PNP0A06:00","env":["ACTION=add","DEVPATH=/devices/system/container/PNP0A06:00","SUBSYSTEM=container","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=898"]}
{"action":"add","devpath":"/devices/system/container/PNP0A06:01","env":["ACTION=add","DEVPATH=/devices/system/container/PNP0A06:01","SUBSYSTEM=container","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=899"]}
{"action":"add","devpath":"/devices/system/container/PNP0A06:02","env":["ACTION=add","DEVPATH=/devices/system/container/PNP0A06:02","SUBSYSTEM=container","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=900"]}
{"action":"add","devpath":"/devices/system/cpu/cpu0","env":["ACTION=add","DEVPATH=/devices/system/cpu/cpu0","SUBSYSTEM=cpu","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=processor","MODALIAS=cpu:type:x86,ven0002fam000Fmod006B:feature:,0000,0002,0003,0004,0005,0006,0007,0008,0009,000B,000C,000D,000E,000F,0010,0011,0013,0017,0018,0019,001A,0020,0022,0023,0024,0025,0026,0027,0028,0029,002B,002C,002D,002E,002F,0030,0031,0034,0037,0038,003D,0064,006E,0070,0074,0075,0076,0079,007A,007F,0080,008D,0095,009F,00C0,00C8,00ED,00F3,010F,0115,0165,016C,0282\n","SEQNUM=901"]}
{"action":"add","devpath":"/devices/system/machinecheck/machinecheck0","env":["ACTION=add","DEVPATH=/devices/system/machinecheck/machinecheck0","SUBSYSTEM=machinecheck","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=902"]}
{"action":"add","devpath":"/devices/system/memory/memory0","env":["ACTION=add","DEVPATH=/devices/system/memory/memory0","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=903"]}
{"action":"add","devpath":"/devices/system/memory/memory1","env":["ACTION=add","DEVPATH=/devices/system/memory/memory1","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=904"]}
{"action":"add","devpath":"/devices/system/memory/memory2","env":["ACTION=add","DEVPATH=/devices/system/memory/memory2","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=905"]}
{"action":"add","devpath":"/devices/system/memory/memory3","env":["ACTION=add","DEVPATH=/devices/system/memory/memory3","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=906"]}
{"action":"add","devpath":"/devices/system/memory/memory4","env":["ACTION=add","DEVPATH=/devices/system/memory/memory4","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=907"]}
{"action":"add","devpath":"/devices/system/memory/memory5","env":["ACTION=add","DEVPATH=/devices/system/memory/memory5","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=908"]}
{"action":"add","devpath":"/devices/system/memory/memory6","env":["ACTION=add","DEVPATH=/devices/system/memory/memory6","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=909"]}
{"action":"add","devpath":"/devices/system/memory/memory7","env":["ACTION=add","DEVPATH=/devices/system/memory/memory7","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=910"]}
{"action":"add","devpath":"/devices/system/node/node0","env":["ACTION=add","DEVPATH=/devices/system/node/node0","SUBSYSTEM=node","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=911"]}
{"action":"add","devpath":"/devices/tracepoint","env":["ACTION=add","DEVPATH=/devices/tracepoint","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=912"]}
{"action":"add","devpath":"/devices/uprobe","env":["ACTION=add","DEVPATH=/devices/uprobe","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=913"]}
{"action":"add","devpath":"/devices/virtual/bdi/254:0","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/254:0","SUBSYSTEM=bdi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=914"]}
{"action":"add","devpath":"/devices/virtual/devlink/:ata2--scsi:1:0:0:0","env":["ACTION=add","DEVPATH=/devices/virtual/devlink/:ata2--scsi:1:0:0:0","SUBSYSTEM=devlink","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=915"]}
{"action":"add","devpath":"/devices/virtual/dmi/id","env":["ACTION=add","DEVPATH=/devices/virtual/dmi/id","SUBSYSTEM=dmi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=dmi:bvnSeaBIOS:bvrrel-1.17.0-0-gb52ca86e094d-prebuilt.qemu.org:bd04/01/2014:br0.0:svnQEMU:pnStandardPC(i440FX+PIIX,1996):pvrpc-i440fx-10.1:cvnQEMU:ct1:cvrpc-i440fx-10.1:sku:","SEQNUM=916"]}
{"action":"add","devpath":"/devices/virtual/mem/full","env":["ACTION=add","DEVPATH=/devices/virtual/mem/full","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=7","DEVNAME=full","DEVMODE=0666","SEQNUM=917"]}
{"action":"add","devpath":"/devices/virtual/mem/kmsg","env":["ACTION=add","DEVPATH=/devices/virtual/mem/kmsg","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=11","DEVNAME=kmsg","DEVMODE=0644","SEQNUM=918"]}
{"action":"add","devpath":"/devices/virtual/mem/mem","env":["ACTION=add","DEVPATH=/devices/virtual/mem/mem","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=1","DEVNAME=mem","SEQNUM=919"]}
{"action":"add","devpath":"/devices/virtual/mem/null","env":["ACTION=add","DEVPATH=/devices/virtual/mem/null","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=3","DEVNAME=null","DEVMODE=0666","SEQNUM=920"]}
{"action":"add","devpath":"/devices/virtual/mem/port","env":["ACTION=add","DEVPATH=/devices/virtual/mem/port","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=4","DEVNAME=port","SEQNUM=921"]}
{"action":"add","devpath":"/devices/virtual/mem/random","env":["ACTION=add","DEVPATH=/devices/virtual/mem/random","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=8","DEVNAME=random","DEVMODE=0666","SEQNUM=922"]}
{"action":"add","devpath":"/devices/virtual/mem/urandom","env":["ACTION=add","DEVPATH=/devices/virtual/mem/urandom","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=9","DEVNAME=urandom","DEVMODE=0666","SEQNUM=923"]}
{"action":"add","devpath":"/devices/virtual/mem/zero","env":["ACTION=add","DEVPATH=/devices/virtual/mem/zero","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=5","DEVNAME=zero","DEVMODE=0666","SEQNUM=924"]}
{"action":"add","devpath":"/devices/virtual/memory_tiering/memory_tier4","env":["ACTION=add","DEVPATH=/devices/virtual/memory_tiering/memory_tier4","SUBSYSTEM=memory_tiering","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=925"]}
{"action":"add","devpath":"/devices/virtual/misc/cpu_dma_latency","env":["ACTION=add","DEVPATH=/devices/virtual/misc/cpu_dma_latency","SUBSYSTEM=misc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=10","MINOR=259","DEVNAME=cpu_dma_latency","SEQNUM=926"]}
{"action":"add","devpath":"/devices/virtual/misc/hpet","env":["ACTION=add","DEVPATH=/devices/virtual/misc/hpet","SUBSYSTEM=misc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=10","MINOR=228","DEVNAME=hpet","SEQNUM=927"]}
{"action":"add","devpath":"/devices/virtual/misc/snapshot","env":["ACTION=add","DEVPATH=/devices/virtual/misc/snapshot","SUBSYSTEM=misc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=10","MINOR=231","DEVNAME=snapshot","SEQNUM=928"]}
{"action":"add","devpath":"/devices/virtual/misc/udmabuf","env":["ACTION=add","DEVPATH=/devices/virtual/misc/udmabuf","SUBSYSTEM=misc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=10","MINOR=258","DEVNAME=udmabuf","SEQNUM=929"]}
{"action":"add","devpath":"/devices/virtual/misc/userfaultfd","env":["ACTION=add","DEVPATH=/devices/virtual/misc/userfaultfd","SUBSYSTEM=misc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=10","MINOR=257","DEVNAME=userfaultfd","SEQNUM=930"]}
{"action":"add","devpath":"/devices/virtual/misc/vga_arbiter","env":["ACTION=add","DEVPATH=/devices/virtual/misc/vga_arbiter","SUBSYSTEM=misc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=10","MINOR=256","DEVNAME=vga_arbiter","SEQNUM=931"]}
{"action":"add","devpath":"/devices/virtual/net/lo","env":["ACTION=add","DEVPATH=/devices/virtual/net/lo","SUBSYSTEM=net","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","INTERFACE=lo","IFINDEX=1","SEQNUM=932"]}
{"action":"add","devpath":"/devices/virtual/thermal/cooling_device0","env":["ACTION=add","DEVPATH=/devices/virtual/thermal/cooling_device0","SUBSYSTEM=thermal","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=933"]}
{"action":"add","devpath":"/devices/virtual/tty/console","env":["ACTION=add","DEVPATH=/devices/virtual/tty/console","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=5","MINOR=1","DEVNAME=console","SEQNUM=934"]}
{"action":"add","devpath":"/devices/virtual/tty/ptmx","env":["ACTION=add","DEVPATH=/devices/virtual/tty/ptmx","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=5","MINOR=2","DEVNAME=ptmx","DEVMODE=0666","SEQNUM=935"]}
{"action":"add","devpath":"/devices/virtual/tty/tty","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=5","MINOR=0","DEVNAME=tty","DEVMODE=0666","SEQNUM=936"]}
{"action":"add","devpath":"/devices/virtual/tty/tty0","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty0","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=0","DEVNAME=tty0","SEQNUM=937"]}
{"action":"add","devpath":"/devices/virtual/tty/tty1","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty1","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=1","DEVNAME=tty1","SEQNUM=938"]}
{"action":"add","devpath":"/devices/virtual/tty/tty10","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty10","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=10","DEVNAME=tty10","SEQNUM=939"]}
{"action":"add","devpath":"/devices/virtual/tty/tty11","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty11","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=11","DEVNAME=tty11","SEQNUM=940"]}
{"action":"add","devpath":"/devices/virtual/tty/tty12","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty12","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=12","DEVNAME=tty12","SEQNUM=941"]}
{"action":"add","devpath":"/devices/virtual/tty/tty13","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty13","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=13","DEVNAME=tty13","SEQNUM=942"]}
{"action":"add","devpath":"/devices/virtual/tty/tty14","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty14","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=14","DEVNAME=tty14","SEQNUM=943"]}
{"action":"add","devpath":"/devices/virtual/tty/tty15","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty15","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=15","DEVNAME=tty15","SEQNUM=944"]}
{"action":"add","devpath":"/devices/virtual/tty/tty16","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty16","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=16","DEVNAME=tty16","SEQNUM=945"]}
{"action":"add","devpath":"/devices/virtual/tty/tty17","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty17","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=17","DEVNAME=tty17","SEQNUM=946"]}
{"action":"add","devpath":"/devices/virtual/tty/tty18","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty18","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=18","DEVNAME=tty18","SEQNUM=947"]}
{"action":"add","devpath":"/devices/virtual/tty/tty19","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty19","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=19","DEVNAME=tty19","SEQNUM=948"]}
{"action":"add","devpath":"/devices/virtual/tty/tty2","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty2","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=2","DEVNAME=tty2","SEQNUM=949"]}
{"action":"add","devpath":"/devices/virtual/tty/tty20","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty20","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=20","DEVNAME=tty20","SEQNUM=950"]}
{"action":"add","devpath":"/devices/virtual/tty/tty21","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty21","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=21","DEVNAME=tty21","SEQNUM=951"]}
{"action":"add","devpath":"/devices/virtual/tty/tty22","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty22","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=22","DEVNAME=tty22","SEQNUM=952"]}
{"action":"add","devpath":"/devices/virtual/tty/tty23","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty23","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=23","DEVNAME=tty23","SEQNUM=953"]}
{"action":"add","devpath":"/devices/virtual/tty/tty24","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty24","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=24","DEVNAME=tty24","SEQNUM=954"]}
{"action":"add","devpath":"/devices/virtual/tty/tty25","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty25","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=25","DEVNAME=tty25","SEQNUM=955"]}
{"action":"add","devpath":"/devices/virtual/tty/tty26","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty26","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=26","DEVNAME=tty26","SEQNUM=956"]}
{"action":"add","devpath":"/devices/virtual/tty/tty27","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty27","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=27","DEVNAME=tty27","SEQNUM=957"]}
{"action":"add","devpath":"/devices/virtual/tty/tty28","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty28","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=28","DEVNAME=tty28","SEQNUM=958"]}
{"action":"add","devpath":"/devices/virtual/tty/tty29","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty29","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=29","DEVNAME=tty29","SEQNUM=959"]}
{"action":"add","devpath":"/devices/virtual/tty/tty3","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty3","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=3","DEVNAME=tty3","SEQNUM=960"]}
{"action":"add","devpath":"/devices/virtual/tty/tty30","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty30","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=30","DEVNAME=tty30","SEQNUM=961"]}
{"action":"add","devpath":"/devices/virtual/tty/tty31","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty31","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=31","DEVNAME=tty31","SEQNUM=962"]}
{"action":"add","devpath":"/devices/virtual/tty/tty32","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty32","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=32","DEVNAME=tty32","SEQNUM=963"]}
{"action":"add","devpath":"/devices/virtual/tty/tty33","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty33","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=33","DEVNAME=tty33","SEQNUM=964"]}
{"action":"add","devpath":"/devices/virtual/tty/tty34","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty34","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=34","DEVNAME=tty34","SEQNUM=965"]}
{"action":"add","devpath":"/devices/virtual/tty/tty35","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty35","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=35","DEVNAME=tty35","SEQNUM=966"]}
{"action":"add","devpath":"/devices/virtual/tty/tty36","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty36","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=36","DEVNAME=tty36","SEQNUM=967"]}
{"action":"add","devpath":"/devices/virtual/tty/tty37","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty37","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=37","DEVNAME=tty37","SEQNUM=968"]}
{"action":"add","devpath":"/devices/virtual/tty/tty38","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty38","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=38","DEVNAME=tty38","SEQNUM=969"]}
{"action":"add","devpath":"/devices/virtual/tty/tty39","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty39","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=39","DEVNAME=tty39","SEQNUM=970"]}
{"action":"add","devpath":"/devices/virtual/tty/tty4","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty4","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=4","DEVNAME=tty4","SEQNUM=971"]}
{"action":"add","devpath":"/devices/virtual/tty/tty40","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty40","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=40","DEVNAME=tty40","SEQNUM=972"]}
{"action":"add","devpath":"/devices/virtual/tty/tty41","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty41","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=41","DEVNAME=tty41","SEQNUM=973"]}
{"action":"add","devpath":"/devices/virtual/tty/tty42","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty42","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=42","DEVNAME=tty42","SEQNUM=974"]}
{"action":"add","devpath":"/devices/virtual/tty/tty43","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty43","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=43","DEVNAME=tty43","SEQNUM=975"]}
{"action":"add","devpath":"/devices/virtual/tty/tty44","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty44","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=44","DEVNAME=tty44","SEQNUM=976"]}
{"action":"add","devpath":"/devices/virtual/tty/tty45","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty45","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=45","DEVNAME=tty45","SEQNUM=977"]}
{"action":"add","devpath":"/devices/virtual/tty/tty46","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty46","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=46","DEVNAME=tty46","SEQNUM=978"]}
{"action":"add","devpath":"/devices/virtual/tty/tty47","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty47","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=47","DEVNAME=tty47","SEQNUM=979"]}
{"action":"add","devpath":"/devices/virtual/tty/tty48","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty48","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=48","DEVNAME=tty48","SEQNUM=980"]}
{"action":"add","devpath":"/devices/virtual/tty/tty49","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty49","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=49","DEVNAME=tty49","SEQNUM=981"]}
{"action":"add","devpath":"/devices/virtual/tty/tty5","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty5","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=5","DEVNAME=tty5","SEQNUM=982"]}
{"action":"add","devpath":"/devices/virtual/tty/tty50","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty50","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=50","DEVNAME=tty50","SEQNUM=983"]}
{"action":"add","devpath":"/devices/virtual/tty/tty51","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty51","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=51","DEVNAME=tty51","SEQNUM=984"]}
{"action":"add","devpath":"/devices/virtual/tty/tty52","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty52","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=52","DEVNAME=tty52","SEQNUM=985"]}
{"action":"add","devpath":"/devices/virtual/tty/tty53","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty53","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=53","DEVNAME=tty53","SEQNUM=986"]}
{"action":"add","devpath":"/devices/virtual/tty/tty54","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty54","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=54","DEVNAME=tty54","SEQNUM=987"]}
{"action":"add","devpath":"/devices/virtual/tty/tty55","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty55","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=55","DEVNAME=tty55","SEQNUM=988"]}
{"action":"add","devpath":"/devices/virtual/tty/tty56","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty56","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=56","DEVNAME=tty56","SEQNUM=989"]}
{"action":"add","devpath":"/devices/virtual/tty/tty57","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty57","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=57","DEVNAME=tty57","SEQNUM=990"]}
{"action":"add","devpath":"/devices/virtual/tty/tty58","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty58","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=58","DEVNAME=tty58","SEQNUM=991"]}
{"action":"add","devpath":"/devices/virtual/tty/tty59","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty59","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=59","DEVNAME=tty59","SEQNUM=992"]}
{"action":"add","devpath":"/devices/virtual/tty/tty6","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty6","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=6","DEVNAME=tty6","SEQNUM=993"]}
{"action":"add","devpath":"/devices/virtual/tty/tty60","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty60","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=60","DEVNAME=tty60","SEQNUM=994"]}
{"action":"add","devpath":"/devices/virtual/tty/tty61","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty61","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=61","DEVNAME=tty61","SEQNUM=995"]}
{"action":"add","devpath":"/devices/virtual/tty/tty62","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty62","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=62","DEVNAME=tty62","SEQNUM=996"]}
{"action":"add","devpath":"/devices/virtual/tty/tty63","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty63","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=63","DEVNAME=tty63","SEQNUM=997"]}
{"action":"add","devpath":"/devices/virtual/tty/tty7","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty7","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=7","DEVNAME=tty7","SEQNUM=998"]}
{"action":"add","devpath":"/devices/virtual/tty/tty8","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty8","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=8","DEVNAME=tty8","SEQNUM=999"]}
{"action":"add","devpath":"/devices/virtual/tty/tty9","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty9","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=9","DEVNAME=tty9","SEQNUM=1000"]}
{"action":"add","devpath":"/devices/virtual/vc/vcs","env":["ACTION=add","DEVPATH=/devices/virtual/vc/vcs","SUBSYSTEM=vc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=7","MINOR=0","DEVNAME=vcs","SEQNUM=1001"]}
{"action":"add","devpath":"/devices/virtual/vc/vcs1","env":["ACTION=add","DEVPATH=/devices/virtual/vc/vcs1","SUBSYSTEM=vc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=7","MINOR=1","DEVNAME=vcs1","SEQNUM=1002"]}
{"action":"add","devpath":"/devices/virtual/vc/vcsa","env":["ACTION=add","DEVPATH=/devices/virtual/vc/vcsa","SUBSYSTEM=vc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=7","MINOR=128","DEVNAME=vcsa","SEQNUM=1003"]}
{"action":"add","devpath":"/devices/virtual/vc/vcsa1","env":["ACTION=add","DEVPATH=/devices/virtual/vc/vcsa1","SUBSYSTEM=vc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=7","MINOR=129","DEVNAME=vcsa1","SEQNUM=1004"]}
{"action":"add","devpath":"/devices/virtual/vc/vcsu","env":["ACTION=add","DEVPATH=/devices/virtual/vc/vcsu","SUBSYSTEM=vc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=7","MINOR=64","DEVNAME=vcsu","SEQNUM=1005"]}
{"action":"add","devpath":"/devices/virtual/vc/vcsu1","env":["ACTION=add","DEVPATH=/devices/virtual/vc/vcsu1","SUBSYSTEM=vc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=7","MINOR=65","DEVNAME=vcsu1","SEQNUM=1006"]}
{"action":"add","devpath":"/devices/virtual/vtconsole/vtcon0","env":["ACTION=add","DEVPATH=/devices/virtual/vtconsole/vtcon0","SUBSYSTEM=vtconsole","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1007"]}
{"action":"add","devpath":"/devices/virtual/workqueue/nvme-auth-wq","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/nvme-auth-wq","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1008"]}
{"action":"add","devpath":"/devices/virtual/workqueue/nvme-delete-wq","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/nvme-delete-wq","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1009"]}
{"action":"add","devpath":"/devices/virtual/workqueue/nvme-reset-wq","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/nvme-reset-wq","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1010"]}
{"action":"add","devpath":"/devices/virtual/workqueue/nvme-wq","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/nvme-wq","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1011"]}
{"action":"add","devpath":"/devices/virtual/workqueue/scsi_tmf_0","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/scsi_tmf_0","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1012"]}
{"action":"add","devpath":"/devices/virtual/workqueue/scsi_tmf_1","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/scsi_tmf_1","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1013"]}
{"action":"add","devpath":"/devices/virtual/workqueue/writeback","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/writeback","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1014"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/wakeup/wakeup8","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/wakeup/wakeup8","SUBSYSTEM=wakeup","SEQNUM=1015"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:04.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:04.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:04.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1016"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:04.0/virtio1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1017"]}
{"action":"bind","devpath":"/devices/pci0000:00/0000:00:04.0","env":["ACTION=bind","DEVPATH=/devices/pci0000:00/0000:00:04.0","SUBSYSTEM=pci","DRIVER=virtio-pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:04.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1018"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","SUBSYSTEM=wakeup","SEQNUM=1019"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1020"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:05.0/virtio2","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:05.0/virtio2","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1021"]}
{"action":"bind","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=bind","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","DRIVER=virtio-pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1022"]}
{"action":"remove","devpath":"/devices/pci0000:00/0000:00:04.0/virtio1","env":["ACTION=remove","DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1023"]}
{"action":"unbind","devpath":"/devices/pci0000:00/0000:00:04.0","env":["ACTION=unbind","DEVPATH=/devices/pci0000:00/0000:00:04.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:04.0","SEQNUM=1024"]}
{"action":"remove","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/wakeup/wakeup8","env":["ACTION=remove","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/wakeup/wakeup8","SUBSYSTEM=wakeup","SEQNUM=1025"]}
{"action":"remove","devpath":"/devices/pci0000:00/0000:00:04.0","env":["ACTION=remove","DEVPATH=/devices/pci0000:00/0000:00:04.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:04.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1026"]}
{"action":"remove","devpath":"/devices/pci0000:00/0000:00:05.0/virtio2","env":["ACTION=remove","DEVPATH=/devices/pci0000:00/0000:00:05.0/virtio2","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1027"]}
{"action":"unbind","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=unbind","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","SEQNUM=1028"]}
{"action":"remove","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","env":["ACTION=remove","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","SUBSYSTEM=wakeup","SEQNUM=1029"]}
{"action":"remove","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=remove","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1030"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/wakeup/wakeup8","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/wakeup/wakeup8","SUBSYSTEM=wakeup","SEQNUM=1031"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:04.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:04.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:04.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1032"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:04.0/virtio1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1033"]}
{"action":"bind","devpath":"/devices/pci0000:00/0000:00:04.0","env":["ACTION=bind","DEVPATH=/devices/pci0000:00/0000:00:04.0","SUBSYSTEM=pci","DRIVER=virtio-pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:04.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1034"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","SUBSYSTEM=wakeup","SEQNUM=1035"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1036"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:05.0/virtio2","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:05.0/virtio2","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1037"]}
{"action":"bind","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=bind","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","DRIVER=virtio-pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1038"]}
{"action":"remove","devpath":"/devices/pci0000:00/0000:00:05.0/virtio2","env":["ACTION=remove","DEVPATH=/devices/pci0000:00/0000:00:05.0/virtio2","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1039"]}
{"action":"unbind","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=unbind","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","SEQNUM=1040"]}
{"action":"remove","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","env":["ACTION=remove","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","SUBSYSTEM=wakeup","SEQNUM=1041"]}
{"action":"remove","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=remove","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1042"]}
+1
View File
@@ -0,0 +1 @@
{"action":"move","devpath":"/devices/virtual/net/_lo","env":["ACTION=move","DEVPATH=/devices/virtual/net/_lo","SUBSYSTEM=net","DEVPATH_OLD=/devices/virtual/net/lo","INTERFACE=_lo","IFINDEX=1","SEQNUM=1043"]}
+8
View File
@@ -0,0 +1,8 @@
{"action":"remove","devpath":"/devices/system/machinecheck/machinecheck0","env":["ACTION=remove","DEVPATH=/devices/system/machinecheck/machinecheck0","SUBSYSTEM=machinecheck","SEQNUM=1044"]}
{"action":"offline","devpath":"/devices/system/cpu/cpu0","env":["ACTION=offline","DEVPATH=/devices/system/cpu/cpu0","SUBSYSTEM=cpu","DRIVER=processor","MODALIAS=cpu:type:x86,ven0002fam000Fmod006B:feature:,0000,0002,0003,0004,0005,0006,0007,0008,0009,000B,000C,000D,000E,000F,0010,0011,0013,0017,0018,0019,001A,001C,0020,0022,0023,0024,0025,0026,0027,0028,0029,002B,002C,002D,002E,002F,0030,0031,0034,0037,0038,003D,0064,006E,0070,0074,0075,0076,0079,007A,007F,0080,008D,0095,009F,00C0,00C1,00C8,00ED,00F3,010F,0115,0165,016C,0282\n","SEQNUM=1045"]}
{"action":"add","devpath":"/devices/system/machinecheck/machinecheck0","env":["ACTION=add","DEVPATH=/devices/system/machinecheck/machinecheck0","SUBSYSTEM=machinecheck","SEQNUM=1046"]}
{"action":"online","devpath":"/devices/system/cpu/cpu0","env":["ACTION=online","DEVPATH=/devices/system/cpu/cpu0","SUBSYSTEM=cpu","DRIVER=processor","MODALIAS=cpu:type:x86,ven0002fam000Fmod006B:feature:,0000,0002,0003,0004,0005,0006,0007,0008,0009,000B,000C,000D,000E,000F,0010,0011,0013,0017,0018,0019,001A,001C,0020,0022,0023,0024,0025,0026,0027,0028,0029,002B,002C,002D,002E,002F,0030,0031,0034,0037,0038,003D,0064,006E,0070,0074,0075,0076,0079,007A,007F,0080,008D,0095,009F,00C0,00C1,00C8,00ED,00F3,010F,0115,0165,016C,0282\n","SEQNUM=1047"]}
{"action":"remove","devpath":"/devices/system/machinecheck/machinecheck0","env":["ACTION=remove","DEVPATH=/devices/system/machinecheck/machinecheck0","SUBSYSTEM=machinecheck","SEQNUM=1048"]}
{"action":"offline","devpath":"/devices/system/cpu/cpu0","env":["ACTION=offline","DEVPATH=/devices/system/cpu/cpu0","SUBSYSTEM=cpu","DRIVER=processor","MODALIAS=cpu:type:x86,ven0002fam000Fmod006B:feature:,0000,0002,0003,0004,0005,0006,0007,0008,0009,000B,000C,000D,000E,000F,0010,0011,0013,0017,0018,0019,001A,001C,0020,0022,0023,0024,0025,0026,0027,0028,0029,002B,002C,002D,002E,002F,0030,0031,0034,0037,0038,003D,0064,006E,0070,0074,0075,0076,0079,007A,007F,0080,008D,0095,009F,00C0,00C1,00C8,00ED,00F3,010F,0115,0165,016C,0282\n","SEQNUM=1049"]}
{"action":"add","devpath":"/devices/system/machinecheck/machinecheck0","env":["ACTION=add","DEVPATH=/devices/system/machinecheck/machinecheck0","SUBSYSTEM=machinecheck","SEQNUM=1050"]}
{"action":"online","devpath":"/devices/system/cpu/cpu0","env":["ACTION=online","DEVPATH=/devices/system/cpu/cpu0","SUBSYSTEM=cpu","DRIVER=processor","MODALIAS=cpu:type:x86,ven0002fam000Fmod006B:feature:,0000,0002,0003,0004,0005,0006,0007,0008,0009,000B,000C,000D,000E,000F,0010,0011,0013,0017,0018,0019,001A,001C,0020,0022,0023,0024,0025,0026,0027,0028,0029,002B,002C,002D,002E,002F,0030,0031,0034,0037,0038,003D,0064,006E,0070,0074,0075,0076,0079,007A,007F,0080,008D,0095,009F,00C0,00C1,00C8,00ED,00F3,010F,0115,0165,016C,0282\n","SEQNUM=1051"]}
+19
View File
@@ -0,0 +1,19 @@
{"action":"add","devpath":"/devices/virtual/misc/loop-control","env":["ACTION=add","DEVPATH=/devices/virtual/misc/loop-control","SUBSYSTEM=misc","MAJOR=10","MINOR=237","DEVNAME=loop-control","SEQNUM=1052"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:0","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:0","SUBSYSTEM=bdi","SEQNUM=1053"]}
{"action":"add","devpath":"/devices/virtual/block/loop0","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop0","SUBSYSTEM=block","MAJOR=7","MINOR=0","DEVNAME=loop0","DEVTYPE=disk","DISKSEQ=2","SEQNUM=1054"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:1","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:1","SUBSYSTEM=bdi","SEQNUM=1055"]}
{"action":"add","devpath":"/devices/virtual/block/loop1","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop1","SUBSYSTEM=block","MAJOR=7","MINOR=1","DEVNAME=loop1","DEVTYPE=disk","DISKSEQ=3","SEQNUM=1056"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:2","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:2","SUBSYSTEM=bdi","SEQNUM=1057"]}
{"action":"add","devpath":"/devices/virtual/block/loop2","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop2","SUBSYSTEM=block","MAJOR=7","MINOR=2","DEVNAME=loop2","DEVTYPE=disk","DISKSEQ=4","SEQNUM=1058"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:3","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:3","SUBSYSTEM=bdi","SEQNUM=1059"]}
{"action":"add","devpath":"/devices/virtual/block/loop3","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop3","SUBSYSTEM=block","MAJOR=7","MINOR=3","DEVNAME=loop3","DEVTYPE=disk","DISKSEQ=5","SEQNUM=1060"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:4","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:4","SUBSYSTEM=bdi","SEQNUM=1061"]}
{"action":"add","devpath":"/devices/virtual/block/loop4","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop4","SUBSYSTEM=block","MAJOR=7","MINOR=4","DEVNAME=loop4","DEVTYPE=disk","DISKSEQ=6","SEQNUM=1062"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:5","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:5","SUBSYSTEM=bdi","SEQNUM=1063"]}
{"action":"add","devpath":"/devices/virtual/block/loop5","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop5","SUBSYSTEM=block","MAJOR=7","MINOR=5","DEVNAME=loop5","DEVTYPE=disk","DISKSEQ=7","SEQNUM=1064"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:6","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:6","SUBSYSTEM=bdi","SEQNUM=1065"]}
{"action":"add","devpath":"/devices/virtual/block/loop6","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop6","SUBSYSTEM=block","MAJOR=7","MINOR=6","DEVNAME=loop6","DEVTYPE=disk","DISKSEQ=8","SEQNUM=1066"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:7","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:7","SUBSYSTEM=bdi","SEQNUM=1067"]}
{"action":"add","devpath":"/devices/virtual/block/loop7","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop7","SUBSYSTEM=block","MAJOR=7","MINOR=7","DEVNAME=loop7","DEVTYPE=disk","DISKSEQ=9","SEQNUM=1068"]}
{"action":"add","devpath":"/module/loop","env":["ACTION=add","DEVPATH=/module/loop","SUBSYSTEM=module","SEQNUM=1069"]}
{"action":"change","devpath":"/devices/virtual/block/loop0","env":["ACTION=change","DEVPATH=/devices/virtual/block/loop0","SUBSYSTEM=block","MAJOR=7","MINOR=0","DEVNAME=loop0","DEVTYPE=disk","DISKSEQ=10","SEQNUM=1070"]}
+2
View File
@@ -0,0 +1,2 @@
{"action":"change","devpath":"/devices/virtual/block/loop0","env":["ACTION=change","DEVPATH=/devices/virtual/block/loop0","SUBSYSTEM=block","MAJOR=7","MINOR=0","DEVNAME=loop0","DEVTYPE=disk","DISKSEQ=10","SEQNUM=1071"]}
{"action":"change","devpath":"/devices/virtual/block/loop0","env":["ACTION=change","DEVPATH=/devices/virtual/block/loop0","SUBSYSTEM=block","DISK_MEDIA_CHANGE=1","MAJOR=7","MINOR=0","DEVNAME=loop0","DEVTYPE=disk","DISKSEQ=10","SEQNUM=1072"]}
+18
View File
@@ -0,0 +1,18 @@
{"action":"remove","devpath":"/devices/virtual/misc/loop-control","env":["ACTION=remove","DEVPATH=/devices/virtual/misc/loop-control","SUBSYSTEM=misc","MAJOR=10","MINOR=237","DEVNAME=loop-control","SEQNUM=1073"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:0","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:0","SUBSYSTEM=bdi","SEQNUM=1074"]}
{"action":"remove","devpath":"/devices/virtual/block/loop0","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop0","SUBSYSTEM=block","MAJOR=7","MINOR=0","DEVNAME=loop0","DEVTYPE=disk","DISKSEQ=11","SEQNUM=1075"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:1","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:1","SUBSYSTEM=bdi","SEQNUM=1076"]}
{"action":"remove","devpath":"/devices/virtual/block/loop1","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop1","SUBSYSTEM=block","MAJOR=7","MINOR=1","DEVNAME=loop1","DEVTYPE=disk","DISKSEQ=3","SEQNUM=1077"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:2","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:2","SUBSYSTEM=bdi","SEQNUM=1078"]}
{"action":"remove","devpath":"/devices/virtual/block/loop2","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop2","SUBSYSTEM=block","MAJOR=7","MINOR=2","DEVNAME=loop2","DEVTYPE=disk","DISKSEQ=4","SEQNUM=1079"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:3","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:3","SUBSYSTEM=bdi","SEQNUM=1080"]}
{"action":"remove","devpath":"/devices/virtual/block/loop3","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop3","SUBSYSTEM=block","MAJOR=7","MINOR=3","DEVNAME=loop3","DEVTYPE=disk","DISKSEQ=5","SEQNUM=1081"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:4","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:4","SUBSYSTEM=bdi","SEQNUM=1082"]}
{"action":"remove","devpath":"/devices/virtual/block/loop4","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop4","SUBSYSTEM=block","MAJOR=7","MINOR=4","DEVNAME=loop4","DEVTYPE=disk","DISKSEQ=6","SEQNUM=1083"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:5","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:5","SUBSYSTEM=bdi","SEQNUM=1084"]}
{"action":"remove","devpath":"/devices/virtual/block/loop5","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop5","SUBSYSTEM=block","MAJOR=7","MINOR=5","DEVNAME=loop5","DEVTYPE=disk","DISKSEQ=7","SEQNUM=1085"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:6","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:6","SUBSYSTEM=bdi","SEQNUM=1086"]}
{"action":"remove","devpath":"/devices/virtual/block/loop6","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop6","SUBSYSTEM=block","MAJOR=7","MINOR=6","DEVNAME=loop6","DEVTYPE=disk","DISKSEQ=8","SEQNUM=1087"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:7","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:7","SUBSYSTEM=bdi","SEQNUM=1088"]}
{"action":"remove","devpath":"/devices/virtual/block/loop7","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop7","SUBSYSTEM=block","MAJOR=7","MINOR=7","DEVNAME=loop7","DEVTYPE=disk","DISKSEQ=9","SEQNUM=1089"]}
{"action":"remove","devpath":"/module/loop","env":["ACTION=remove","DEVPATH=/module/loop","SUBSYSTEM=module","SEQNUM=1090"]}
+1 -1
View File
@@ -40,7 +40,7 @@ func TestTransform(t *testing.T) {
const maxChunkWords = 8 << 10
buf := make([]byte, 2*maxChunkWords*8)
for i := uint64(0); i < 2*maxChunkWords; i++ {
for i := range uint64(2 * maxChunkWords) {
binary.LittleEndian.PutUint64(buf[i*8:], i)
}
if err := lockedfile.Write(path, bytes.NewReader(buf[:8]), 0666); err != nil {
+1 -2
View File
@@ -58,8 +58,7 @@ func (k *outcome) finalise(
supp := make([]string, len(config.Groups))
for i, name := range config.Groups {
if gid, err := k.lookupGroupId(name); err != nil {
var unknownGroupError user.UnknownGroupError
if errors.As(err, &unknownGroupError) {
if unknownGroupError, ok := errors.AsType[user.UnknownGroupError](err); ok {
return newWithMessageError(fmt.Sprintf("unknown group %q", name), unknownGroupError)
} else {
return &hst.AppError{Step: "look up group by name", Err: err, Msg: err.Error()}
+4 -6
View File
@@ -51,18 +51,16 @@ func (h *Hsu) ID() (int, error) {
cmd.Stderr = os.Stderr // pass through fatal messages
cmd.Env = make([]string, 0)
cmd.Dir = fhs.Root
var (
p []byte
exitError *exec.ExitError
)
var p []byte
const step = "obtain uid from hsu"
if p, h.idErr = h.k.cmdOutput(cmd); h.idErr == nil {
h.id, h.idErr = strconv.Atoi(string(p))
if h.idErr != nil {
h.idErr = &hst.AppError{Step: step, Err: h.idErr, Msg: "invalid uid string from hsu"}
}
} else if errors.As(h.idErr, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
} else if exitError, ok := errors.AsType[*exec.ExitError](h.idErr); ok &&
exitError != nil &&
exitError.ExitCode() == 1 {
// hsu prints an error message in this case
h.idErr = &hst.AppError{Step: step, Err: ErrHsuAccess}
} else if errors.Is(h.idErr, os.ErrNotExist) {
+3 -3
View File
@@ -328,11 +328,11 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
}
if err := k.sys.Revert((*system.Criteria)(&ec)); err != nil {
var joinError interface {
joinError, ok := errors.AsType[interface {
Unwrap() []error
error
}
if !errors.As(err, &joinError) || joinError == nil {
}](err)
if !ok || joinError == nil {
perror(err, "revert system setup")
} else {
for _, v := range joinError.Unwrap() {
+2 -2
View File
@@ -390,8 +390,8 @@ func shimEntrypoint(k syscallDispatcher) {
if err := k.containerWait(z); err != nil {
sp.destroy()
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
exitError, ok := errors.AsType[*exec.ExitError](err)
if !ok {
if errors.Is(err, context.Canceled) {
k.exit(hst.ExitCancel)
}
+4 -4
View File
@@ -176,8 +176,8 @@ func marshalValueAppendRaw(data []byte, v reflect.Value) ([]byte, error) {
case reflect.Struct:
data = SPA_TYPE_Struct.append(data)
var err error
for i := 0; i < v.NumField(); i++ {
data, err = marshalValueAppend(data, v.Field(i))
for _, field := range v.Fields() {
data, err = marshalValueAppend(data, field)
if err != nil {
return data, err
}
@@ -370,8 +370,8 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error {
}
var fieldWireSize Word
for i := 0; i < v.NumField(); i++ {
if err := unmarshalValue(data, v.Field(i), &fieldWireSize); err != nil {
for _, field := range v.Fields() {
if err := unmarshalValue(data, field, &fieldWireSize); err != nil {
return err
}
// bounds check completed in successful call to unmarshalValue
+405
View File
@@ -0,0 +1,405 @@
package pkg
import (
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"unsafe"
"hakurei.app/check"
)
/*
| mode uint32 | path_sz uint32 |
| data_sz uint64 |
| path string |
| data []byte |
*/
// An ArchiveHeader represents a single header in an archive.
type ArchiveHeader struct {
Mode fs.FileMode // file mode bits
Path string // pathname of the file
Size uint64 // size of data segment
}
// Writer implements sequential writing of an archive. [Writer.WriteHeader]
// begins a new file with the provided [ArchiveHeader], and then Writer can be
// treated as an [io.Writer] to supply that file's data.
//
// It is the caller's responsibility to write entries in lexical order.
type Writer struct {
// Underlying writer.
w io.Writer
// Current header.
h ArchiveHeader
// Fixed-size header segment.
buf [wordSize * 2]byte
// Current position in data segment.
n uint64
}
// NewWriter returns the address of a new [Writer] writing to w.
func NewWriter(w io.Writer) *Writer { return &Writer{w: w} }
var zero [wordSize]byte
// padSize returns the padding size for aligning sz.
func padSize[T int | uint64](sz T) T {
return (wordSize - (sz)%wordSize) % wordSize
}
// flush concludes writing to the current file and writes padding.
func (aw *Writer) flush() error {
if aw.h.Size > aw.n {
return fmt.Errorf("missed writing %d bytes", aw.h.Size-aw.n)
} else if aw.h.Size < aw.n {
return fmt.Errorf("wrote %d bytes beyond end of file", aw.n-aw.h.Size)
}
if psz := padSize(aw.h.Size); psz != 0 {
if _, err := aw.w.Write(zero[:psz]); err != nil {
return err
}
}
aw.n = 0
return nil
}
// WriteHeader writes h and begins accepting its corresponding file.
func (aw *Writer) WriteHeader(h *ArchiveHeader) error {
if err := aw.flush(); err != nil {
return err
}
aw.h = *h
binary.LittleEndian.PutUint32(aw.buf[:], uint32(aw.h.Mode))
binary.LittleEndian.PutUint32(aw.buf[wordSize/2:], uint32(len(aw.h.Path)))
binary.LittleEndian.PutUint64(aw.buf[wordSize:], aw.h.Size)
if _, err := aw.w.Write(aw.buf[:]); err != nil {
return err
} else if _, err = aw.w.Write(
unsafe.Slice(unsafe.StringData(aw.h.Path), len(aw.h.Path)),
); err != nil {
return err
} else if psz := padSize(len(aw.h.Path)); psz != 0 {
if _, err = aw.w.Write(zero[:psz]); err != nil {
return err
}
}
return nil
}
// Write writes p to the underlying writer and records the new position. Invalid
// positions are reported by WriteHeader and Close.
func (aw *Writer) Write(p []byte) (n int, err error) {
n, err = aw.w.Write(p)
aw.n += uint64(n)
return
}
// Close concludes writing to the archive stream.
func (aw *Writer) Close() (err error) {
err = aw.flush()
aw.w = nil
return
}
// ErrInsecurePath is returned by [FlatEntry.Decode] if validation is requested
// and a nonlocal path is encountered in the stream.
var ErrInsecurePath = errors.New("insecure file path")
// Reader implements sequential reading of an archive. [Reader.Next] advances to
// the next file in the archive (including the first), and then Reader can be
// treated as an [io.Reader] to access the file's data.
type Reader struct {
// Underlying reader.
r io.Reader
// Fixed-size header segment.
buf [wordSize * 2]byte
// Remaining bytes in current data segment.
n, pad uint64
}
// NewReader returns the address of a new [Reader] reading from r.
func NewReader(r io.Reader) *Reader { return &Reader{r: r} }
// Next advances ar to the next entry. Remaining bytes of the current data
// segment are discarded. Advancing beyond the final entry returns [io.EOF].
func (ar *Reader) Next() (*ArchiveHeader, error) {
if dsz := int64(ar.n + ar.pad); dsz > 0 {
if n, err := io.CopyN(io.Discard, ar.r, dsz); err != nil {
if errors.Is(err, io.EOF) && n != dsz {
err = io.ErrUnexpectedEOF
}
return nil, err
}
}
if _, err := io.ReadFull(ar.r, ar.buf[:]); err != nil {
return nil, err
}
h := ArchiveHeader{
Mode: fs.FileMode(binary.LittleEndian.Uint32(ar.buf[:])),
Size: binary.LittleEndian.Uint64(ar.buf[wordSize:]),
}
pathSize := int(binary.LittleEndian.Uint32(ar.buf[wordSize/2:]))
pPathSize := alignSize(pathSize)
buf := make([]byte, pPathSize)
if _, err := io.ReadFull(ar.r, buf); err != nil {
if errors.Is(err, io.EOF) {
err = io.ErrUnexpectedEOF
}
return nil, err
}
h.Path = unsafe.String(unsafe.SliceData(buf), pathSize)
if !filepath.IsLocal(h.Path) {
return &h, ErrInsecurePath
}
ar.n = h.Size
ar.pad = padSize(h.Size)
return &h, nil
}
// Read implements [io.Reader] for the data segment of the current entry.
func (ar *Reader) Read(p []byte) (n int, err error) {
if uint64(len(p)) > ar.n {
p = p[:ar.n]
}
if len(p) > 0 {
n, err = ar.r.Read(p)
ar.n -= uint64(n)
}
switch err {
case io.EOF:
if ar.n > 0 {
return n, io.ErrUnexpectedEOF
}
case nil:
if ar.n == 0 {
return n, io.EOF
}
}
return
}
// Write writes a deterministic representation of the contents of fsys to w.
// The resulting data can be hashed to produce a deterministic checksum for the
// directory.
func Write(fsys fs.FS, root string, w io.Writer) error {
aw := NewWriter(w)
if err := fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
var fi fs.FileInfo
fi, err = d.Info()
if err != nil {
return err
}
h := ArchiveHeader{
Path: path,
Mode: fi.Mode(),
}
if h.Mode.IsRegular() {
h.Size = uint64(fi.Size())
if err = aw.WriteHeader(&h); err != nil {
return err
}
var r fs.File
r, err = fsys.Open(path)
if err != nil {
return err
}
_, err = io.Copy(aw, r)
if _err := r.Close(); err == nil {
err = _err
}
return err
} else if h.Mode&fs.ModeSymlink != 0 {
var newpath string
if newpath, err = fs.ReadLink(fsys, path); err != nil {
return err
}
h.Size = uint64(len(newpath))
if err = aw.WriteHeader(&h); err != nil {
return err
}
_, err = aw.Write(unsafe.Slice(unsafe.StringData(newpath), len(newpath)))
return err
} else if !h.Mode.IsDir() {
return InvalidFileModeError(h.Mode)
}
return aw.WriteHeader(&h)
}); err != nil {
return err
}
return aw.Close()
}
// SumFS saves checksum of the archive of fsys to the value pointed to by buf.
func SumFS(buf *Checksum, fsys fs.FS, root string) error {
h := sha512.New384()
if err := Write(fsys, root, h); err != nil {
return err
}
h.Sum(buf[:0])
return nil
}
// SumDir saves checksum of the archive of directory at pathname to the value
// pointed to by buf.
func SumDir(buf *Checksum, pathname *check.Absolute) error {
return SumFS(buf, os.DirFS(pathname.String()), ".")
}
// archiveArtifact is an [Artifact] unpacking an archive supported by [Reader]
// backed by a [FileArtifact].
type archiveArtifact struct {
// Caller-supplied backing archive.
f Artifact
}
// NewArchive returns a new [Artifact] backed by the supplied [Artifact]. The
// source [Artifact] must be a [FileArtifact] and produce a stream compatible
// with [Reader].
func NewArchive(a Artifact) Artifact {
return archiveArtifact{a}
}
// Kind returns the hardcoded [Kind] constant.
func (archiveArtifact) Kind() Kind { return KindArchive }
// Params is a noop.
func (archiveArtifact) Params(*IContext) {}
func init() {
register(KindArchive, func(r *IRReader) Artifact {
a := NewArchive(r.Next())
if _, ok := r.Finalise(); ok {
panic(ErrUnexpectedChecksum)
}
return a
})
}
// Dependencies returns a slice containing the backing file.
func (a archiveArtifact) Dependencies() []Artifact {
return []Artifact{a.f}
}
// IsExclusive returns false: [Reader] is fully sequential.
func (archiveArtifact) IsExclusive() bool { return false }
// Cure cures the [Artifact], producing a directory located at work.
func (a archiveArtifact) Cure(t *TContext) (err error) {
var r io.ReadCloser
if r, err = t.Open(a.f); err != nil {
return
}
defer func() {
closeErr := r.Close()
if err == nil {
err = closeErr
}
}()
type dirTargetPerm struct {
path string
mode fs.FileMode
}
var madeDirectories []dirTargetPerm
if err = os.MkdirAll(t.GetWorkDir().String(), 0700); err != nil {
return
}
var root *os.Root
if root, err = os.OpenRoot(t.GetWorkDir().String()); err != nil {
return
}
defer func() {
closeErr := root.Close()
if err == nil {
err = closeErr
}
}()
var header *ArchiveHeader
ar := NewReader(r)
for header, err = ar.Next(); err == nil; header, err = ar.Next() {
if header.Mode.IsRegular() {
var f *os.File
if f, err = root.OpenFile(
header.Path,
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
header.Mode.Perm(),
); err != nil {
return
}
if _, err = io.Copy(f, ar); err != nil {
_ = f.Close()
return
} else if err = f.Close(); err != nil {
return
}
} else if header.Mode&fs.ModeSymlink != 0 {
var p []byte
if p, err = io.ReadAll(ar); err != nil {
return
}
if err = root.Symlink(
unsafe.String(unsafe.SliceData(p), len(p)),
header.Path,
); err != nil {
return
}
} else if header.Mode.IsDir() {
if header.Path == "." {
continue
}
madeDirectories = append(madeDirectories, dirTargetPerm{
path: header.Path,
mode: header.Mode,
})
if err = root.Mkdir(header.Path, 0700); err != nil {
return
}
} else {
return InvalidFileModeError(header.Mode)
}
}
if errors.Is(err, io.EOF) {
err = nil
}
if err == nil {
for _, e := range madeDirectories {
if err = root.Chmod(e.path, e.mode.Perm()); err != nil {
return
}
}
} else {
return
}
return
}
+240
View File
@@ -0,0 +1,240 @@
package pkg_test
import (
"bytes"
"io"
"io/fs"
"maps"
"reflect"
"testing"
"testing/fstest"
"unsafe"
"hakurei.app/check"
"hakurei.app/internal/pkg"
)
func TestArchive(t *testing.T) {
t.Parallel()
type entry struct {
path string
mode fs.FileMode
data string
}
testCases := []struct {
name string
fsys fs.FS
entries []entry
sum pkg.Checksum
err error
}{
{"bad type", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"invalid": {Mode: fs.ModeCharDevice | 0400},
}, nil, pkg.Checksum{}, pkg.InvalidFileModeError(
fs.ModeCharDevice | 0400,
)},
{"coldboot", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"devices": {Mode: fs.ModeDir | 0700},
"devices/uevent": {Mode: 0600, Data: []byte("add")},
"devices/empty": {Mode: fs.ModeDir | 0700},
"devices/sub": {Mode: fs.ModeDir | 0700},
"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"block": {Mode: fs.ModeDir | 0700},
"block/uevent": {Mode: 0600},
}, []entry{
{".", fs.ModeDir | 0700, ""},
{"block", fs.ModeDir | 0700, ""},
{"block/uevent", 0600, ""},
{"devices", fs.ModeDir | 0700, ""},
{"devices/empty", fs.ModeDir | 0700, ""},
{"devices/sub", fs.ModeDir | 0700, ""},
{"devices/sub/uevent", 0600, "add"},
{"devices/uevent", 0600, "add"},
}, pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8"), nil},
{"empty", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"identifier": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []entry{
{".", fs.ModeDir | 0700, ""},
{"checksum", fs.ModeDir | 0700, ""},
{"identifier", fs.ModeDir | 0700, ""},
{"work", fs.ModeDir | 0700, ""},
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C"), nil},
{"sample directory step garbage", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500},
"lib": {Mode: fs.ModeDir | 0500},
"lib/check": {Mode: 0400},
"lib/pkgconfig": {Mode: fs.ModeDir | 0500},
}, []entry{
{".", fs.ModeDir | 0500, ""},
{"lib", fs.ModeDir | 0500, ""},
{"lib/check", 0400, ""},
{"lib/pkgconfig", fs.ModeDir | 0500, ""},
}, pkg.MustDecode("CUx-3hSbTWPsbMfDhgalG4Ni_GmR9TnVX8F99tY_P5GtkYvczg9RrF5zO0jX9XYT"), nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("roundtrip", func(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
if err := pkg.Write(
tc.fsys,
".",
&buf,
); !reflect.DeepEqual(err, tc.err) {
t.Fatalf("Flatten: error = %v, want %v", err, tc.err)
} else if tc.err != nil {
return
}
r := pkg.NewReader(bytes.NewReader(buf.Bytes()))
var got []entry
for {
h, err := r.Next()
if err != nil {
if err == io.EOF {
break
}
t.Fatalf("Next: error = %v", err)
}
var data []byte
if data, err = io.ReadAll(r); err != nil {
t.Fatalf("Read: error = %v", err)
}
got = append(got, entry{
path: h.Path,
mode: h.Mode,
data: unsafe.String(unsafe.SliceData(data), len(data)),
})
}
if !reflect.DeepEqual(got, tc.entries) {
t.Fatalf("Reader: %#v, want %#v", got, tc.entries)
}
})
if tc.err != nil {
return
}
t.Run("hash", func(t *testing.T) {
t.Parallel()
var got pkg.Checksum
if err := pkg.SumFS(&got, tc.fsys, "."); err != nil {
t.Fatalf("SumFS: error = %v", err)
} else if got != tc.sum {
t.Fatalf("SumFS: %v", &pkg.ChecksumMismatchError{
Got: got,
Want: tc.sum,
})
}
})
})
}
}
var archiveTestdata = fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"devices": {Mode: fs.ModeDir | 0700},
"devices/uevent": {Mode: 0600, Data: []byte("add")},
"devices/empty": {Mode: fs.ModeDir | 0700},
"devices/sub": {Mode: fs.ModeDir | 0700},
"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"block": {Mode: fs.ModeDir | 0700},
"block/uevent": {Mode: 0600},
}
func TestArchiveArtifact(t *testing.T) {
t.Parallel()
want := maps.Clone(archiveTestdata)
want["."].Mode = fs.ModeDir | 0500
checkWithCache(t, []cacheTestCase{
{"unpack", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
var buf bytes.Buffer
if err := pkg.Write(archiveTestdata, ".", &buf); err != nil {
t.Fatal(err)
}
cureMany(t, c, []cureStep{
{"sample", pkg.NewArchive(
pkg.NewFile("", buf.Bytes()),
), ignorePathname, expectsFS(want), nil},
})
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F": {Mode: fs.ModeDir | 0500},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/block": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/block/uevent": {Mode: 0600},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/empty": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/sub": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/uevent": {Mode: 0600, Data: []byte("add")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/3oYyAbRJ_we7AgWo1BRcRcnxXFk3mAQ0Qui2nGQMi8GIJNJQtvUC6P2IeoA5mbjD": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F")},
"substitute": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
})
}
func BenchmarkArchiveRead(b *testing.B) {
var buf bytes.Buffer
if err := pkg.Write(archiveTestdata, ".", &buf); err != nil {
b.Fatal(err)
}
testdata := buf.Bytes()
for b.Loop() {
r := pkg.NewReader(bytes.NewReader(testdata))
for {
_, err := r.Next()
if err != nil {
if err == io.EOF {
break
}
b.Fatal(err)
}
}
}
}
func BenchmarkArchiveWrite(b *testing.B) {
for b.Loop() {
if err := pkg.Write(archiveTestdata, ".", io.Discard); err != nil {
b.Fatal(err)
}
}
}
+152
View File
@@ -0,0 +1,152 @@
package pkg
import (
"errors"
"os"
"unique"
)
// Clean destroys checksum backing entries without any identifier or substitute
// entry referring to it. If at least one keep [Artifact] is specified,
// identifier and substitute entries not kept alive by them are destroyed first.
func (c *Cache) Clean(dry, inputs bool, keep ...Artifact) (
[]unique.Handle[ID],
[]unique.Handle[Checksum],
error,
) {
c.identMu.Lock()
defer c.identMu.Unlock()
c.checksumMu.Lock()
defer c.checksumMu.Unlock()
dents, err := os.ReadDir(c.base.Append(dirChecksum).String())
if err != nil {
return nil, nil, err
}
checksums := make(map[unique.Handle[Checksum]]string, len(dents))
var buf Checksum
for _, dent := range dents {
name := dent.Name()
if err = Decode(&buf, name); err != nil {
return nil, nil, err
}
checksums[unique.Make(buf)] = name
}
type identPair struct {
id unique.Handle[ID]
name string
}
dents, err = os.ReadDir(c.base.Append(dirIdentifier).String())
if err != nil {
return nil, nil, err
}
keepIdents := make(map[unique.Handle[ID]]struct{})
if inputs {
for _, id := range Inputs((*Collect)(&keep)) {
keepIdents[id] = struct{}{}
}
} else {
for _, a := range keep {
keepIdents[c.Ident(a)] = struct{}{}
}
}
idents := make([]identPair, 0, len(dents))
for _, dent := range dents {
name := dent.Name()
if err = Decode(&buf, name); err != nil {
return nil, nil, err
}
id := unique.Make(ID(buf))
if _, ok := keepIdents[id]; len(keep) == 0 || ok {
if err = readlinkChecksum(c.base.Append(
dirIdentifier,
name,
), &buf); err != nil {
return nil, nil, err
}
delete(checksums, unique.Make(buf))
continue
}
c.msg.Verbosef("arranging for destruction of %s...", name)
idents = append(idents, identPair{id, name})
}
destroyedIdents := make([]unique.Handle[ID], 0, len(idents))
for _, pair := range idents {
if !dry {
if err = os.Remove(c.base.Append(
dirStatus,
pair.name,
).String()); err != nil && !errors.Is(err, os.ErrNotExist) {
return destroyedIdents, nil, err
}
if err = os.Remove(c.base.Append(
dirIdentifier,
pair.name,
).String()); err != nil {
return destroyedIdents, nil, err
}
}
destroyedIdents = append(destroyedIdents, pair.id)
}
destroyedChecksums := make([]unique.Handle[Checksum], 0, len(checksums))
for checksum, name := range checksums {
if err = c.parent.Err(); err != nil {
return destroyedIdents, destroyedChecksums, err
}
c.msg.Verbosef("destroying checksum %s...", name)
if !dry {
if err = errors.Join(removeAll(c.base.Append(
dirChecksum,
name,
))); err != nil {
return destroyedIdents, destroyedChecksums, err
}
}
destroyedChecksums = append(destroyedChecksums, checksum)
}
dents, err = os.ReadDir(c.base.Append(dirSubstitute).String())
if err != nil {
return destroyedIdents, destroyedChecksums, err
}
for _, dent := range dents {
name := dent.Name()
if err = readlinkChecksum(c.base.Append(
dirSubstitute,
name,
), &buf); err != nil {
return destroyedIdents, destroyedChecksums, err
}
if _, ok := checksums[unique.Make(buf)]; !ok {
continue
}
c.msg.Verbosef("destroying substitute %s...", name)
if !dry {
if err = os.Remove(c.base.Append(
dirStatus,
name,
).String()); err != nil && !errors.Is(err, os.ErrNotExist) {
return destroyedIdents, nil, err
}
if err = os.Remove(c.base.Append(
dirSubstitute,
name,
).String()); err != nil {
return destroyedIdents, destroyedChecksums, err
}
}
}
return destroyedIdents, destroyedChecksums, nil
}
+293
View File
@@ -0,0 +1,293 @@
package pkg_test
import (
"bytes"
"crypto/sha512"
"io/fs"
"log"
"os"
"slices"
"strings"
"testing"
"unique"
"hakurei.app/internal/pkg"
"hakurei.app/message"
)
// formatHandles returns a user-facing string representing h.
func formatHandles[T pkg.ID | pkg.Checksum](handles ...unique.Handle[T]) string {
var buf strings.Builder
for _, h := range handles {
buf.WriteString(pkg.Encode(pkg.Checksum(h.Value())))
buf.WriteString(", ")
}
return strings.TrimSuffix(buf.String(), ", ")
}
func TestClean(t *testing.T) {
t.Parallel()
ic := pkg.NewIR()
testCases := []struct {
name string
a []pkg.Artifact
keep []pkg.Artifact
inputs bool
want expectsFS
wantIdents []unique.Handle[pkg.ID]
wantChecksums []unique.Handle[pkg.Checksum]
}{
{"simple", []pkg.Artifact{
pkg.NewFile("file", nil),
}, nil, false, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/pPRjw2XYgjB5k8dYedwxTBMgHh4_v2JM_G2Vd-skQbAGOOgPsl3CGSUbEF7om_MO": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
"lock": {Mode: 0644},
"variant": {Mode: 0400},
"status": {Mode: fs.ModeDir | 0700},
"substitute": {Mode: fs.ModeDir | 0700},
"fault": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, nil, nil},
{"keep", []pkg.Artifact{
pkg.NewFile("removed-file", []byte("removed file")),
}, []pkg.Artifact{
pkg.NewFile("file", []byte("\xfd")),
}, false, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/KgZ-FjbGuU-XP2QEHInpgv-2Zn0cTH5NqFMgTU0XrSdKmSwyC-3baVs1BMCP5spk": {Mode: 0400, Data: []byte("\xfd")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/FMwSBYw22KqM8jZryfY2ChHXpLuVDdWYyNOYdHvIVYk8ujY6UnGRm5brr2sTTfpD": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/KgZ-FjbGuU-XP2QEHInpgv-2Zn0cTH5NqFMgTU0XrSdKmSwyC-3baVs1BMCP5spk")},
"lock": {Mode: 0644},
"variant": {Mode: 0400},
"status": {Mode: fs.ModeDir | 0700},
"substitute": {Mode: fs.ModeDir | 0700},
"fault": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []unique.Handle[pkg.ID]{
ic.Ident(pkg.NewFile("removed-file", []byte("removed file"))),
}, []unique.Handle[pkg.Checksum]{
unique.Make(sha512.Sum384([]byte("removed file"))),
}},
{"inputs anchored substitute", []pkg.Artifact{
&stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
cure: func(f *pkg.FContext) error {
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), nil, 0444)
},
},
}, []pkg.Artifact{
&stubArtifactF{
kind: pkg.KindExec,
params: []byte("kept"),
deps: []pkg.Artifact{
pkg.NewFile("kept-input", []byte("kept")),
},
cure: func(f *pkg.FContext) error {
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), nil, 0444)
},
},
}, true, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv": {Mode: 0400, Data: []byte("kept")},
"checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE": {Mode: fs.ModeDir | 0500},
"checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE/result": {Mode: 0444},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/Ef8KX6s_rS_nLgze0rj90zKyrGAvOnzyU0DL7nrYQWEG_f4a9pmUKI6HBEMD8AE8": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv")},
"identifier/xoIGLemzLF227e-w_AJcf_1Sgqh2gs3KFgqvOIWUQE-9P_y2vHBMBytL4GRGQqTb": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"lock": {Mode: 0644},
"variant": {Mode: 0400},
"status": {Mode: fs.ModeDir | 0700},
"substitute": {Mode: fs.ModeDir | 0700},
"substitute/4bjS-QjGcSV4nth-W6Vg3-wolKmKgiq4Ld2oRIWcOfy6Wi41XXLAWPoo8FcDx6BH": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"substitute/dzO8FEY9lu4hwRT6BfRZOX-uYGsC_5XH4jEJ7sJyThcmG9J_w1ArOAaUCGfL8wAM": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"fault": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []unique.Handle[pkg.ID]{
ic.Ident(pkg.NewFile("destroyed-input", []byte("destroyed"))),
ic.Ident(&stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
}),
}, []unique.Handle[pkg.Checksum]{
unique.Make(sha512.Sum384([]byte("destroyed"))),
}},
{"inputs", []pkg.Artifact{
&stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
cure: func(f *pkg.FContext) error {
if w, err := f.GetStatusWriter(); err != nil {
return err
} else if _, err = w.Write([]byte("destroyed")); err != nil {
return err
}
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), nil, 0444)
},
},
}, []pkg.Artifact{
&stubArtifactF{
kind: pkg.KindExec,
params: []byte("kept"),
deps: []pkg.Artifact{
pkg.NewFile("kept-input", []byte("kept")),
},
cure: func(f *pkg.FContext) error {
if w, err := f.GetStatusWriter(); err != nil {
return err
} else if _, err = w.Write([]byte("kept")); err != nil {
return err
}
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), []byte{0}, 0444)
},
},
}, true, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W": {Mode: fs.ModeDir | 0500},
"checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W/result": {Mode: 0444, Data: []byte("\x00")},
"checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv": {Mode: 0400, Data: []byte("kept")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/Ef8KX6s_rS_nLgze0rj90zKyrGAvOnzyU0DL7nrYQWEG_f4a9pmUKI6HBEMD8AE8": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv")},
"identifier/xoIGLemzLF227e-w_AJcf_1Sgqh2gs3KFgqvOIWUQE-9P_y2vHBMBytL4GRGQqTb": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W")},
"lock": {Mode: 0644},
"variant": {Mode: 0400},
"status": {Mode: fs.ModeDir | 0700},
"status/4bjS-QjGcSV4nth-W6Vg3-wolKmKgiq4Ld2oRIWcOfy6Wi41XXLAWPoo8FcDx6BH": {Mode: 0400, Data: []byte(statusHeader + "kept")},
"status/xoIGLemzLF227e-w_AJcf_1Sgqh2gs3KFgqvOIWUQE-9P_y2vHBMBytL4GRGQqTb": {Mode: 0400, Data: []byte(statusHeader + "kept")},
"substitute": {Mode: fs.ModeDir | 0700},
"substitute/4bjS-QjGcSV4nth-W6Vg3-wolKmKgiq4Ld2oRIWcOfy6Wi41XXLAWPoo8FcDx6BH": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W")},
"fault": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []unique.Handle[pkg.ID]{
ic.Ident(pkg.NewFile("destroyed-input", []byte("destroyed"))),
ic.Ident(&stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
}),
}, []unique.Handle[pkg.Checksum]{
unique.Make(expectsFS{
".": {Mode: fs.ModeDir | 0500},
"result": {Mode: 0444},
}.hash()),
unique.Make(sha512.Sum384([]byte("destroyed"))),
}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
base := makeBase(t)
msg := message.New(log.New(os.Stderr, "clean: ", 0))
msg.SwapVerbose(testing.Verbose())
c, err := pkg.Open(t.Context(), msg, 0, 0, 0, base)
if err != nil {
t.Fatal(err)
}
t.Cleanup(c.Close)
all := pkg.Collect(slices.Concat(tc.a, tc.keep))
if _, _, err = c.Cure(&all); !pkg.IsCollected(err) {
t.Fatal(err)
}
var (
idents []unique.Handle[pkg.ID]
checksums []unique.Handle[pkg.Checksum]
)
idents, checksums, err = c.Clean(false, tc.inputs, tc.keep...)
if err != nil {
t.Fatalf("Clean: error = %v", err)
}
var buf [2]pkg.Checksum
slices.SortFunc(idents, func(a, b unique.Handle[pkg.ID]) int {
buf[0], buf[1] = a.Value(), b.Value()
return bytes.Compare(buf[0][:], buf[1][:])
})
slices.SortFunc(checksums, func(a, b unique.Handle[pkg.Checksum]) int {
buf[0], buf[1] = a.Value(), b.Value()
return bytes.Compare(buf[0][:], buf[1][:])
})
if !slices.Equal(idents, tc.wantIdents) {
t.Errorf(
"Clean: idents = %s, want %s",
formatHandles(idents...), formatHandles(tc.wantIdents...),
)
}
if !slices.Equal(checksums, tc.wantChecksums) {
t.Errorf(
"Clean: checksums = %s, want %s",
formatHandles(checksums...), formatHandles(tc.wantChecksums...),
)
}
want := tc.want.hash()
var checksum pkg.Checksum
if err = pkg.SumDir(&checksum, base); err != nil {
t.Fatalf("SumDir: error = %v", err)
} else if checksum != want {
t.Error(expectsFrom(base.String()))
}
})
}
}
+119
View File
@@ -0,0 +1,119 @@
package pkg
import (
"compress/bzip2"
"compress/gzip"
"fmt"
"io"
"os"
)
const (
// Gzip denotes a stream compressed via [gzip].
Gzip = iota
// Bzip2 denotes a stream compressed via [bzip2].
Bzip2
)
// A decompressArtifact is a [FileArtifact] decompressing a backing
// [FileArtifact] stream.
type decompressArtifact struct {
// Caller-supplied backing stream.
f Artifact
// Compression on top of the stream.
compress uint32
}
var _ FileArtifact = new(decompressArtifact)
// decompressArtifactNamed embeds decompressArtifact for a [fmt.Stringer] stream.
type decompressArtifactNamed struct {
decompressArtifact
// Copied from decompressArtifact.f.
name string
}
var _ fmt.Stringer = new(decompressArtifactNamed)
// NewDecompress returns a [FileArtifact] decompressing the supplied [Artifact].
func NewDecompress(a Artifact, compress uint32) FileArtifact {
da := decompressArtifact{a, compress}
if s, ok := a.(fmt.Stringer); ok {
if name := s.String(); name != "" {
return &decompressArtifactNamed{da, name}
}
}
return &da
}
// String returns the name of the underlying [Artifact] prefixed with decompress.
func (a *decompressArtifactNamed) String() string { return "decompress-" + a.name }
// Kind returns the hardcoded [Kind] constant.
func (a *decompressArtifact) Kind() Kind { return KindDecompress }
// Params writes value of compression enum.
func (a *decompressArtifact) Params(ctx *IContext) { ctx.WriteUint32(a.compress) }
func init() {
register(KindDecompress, func(r *IRReader) Artifact {
a := NewDecompress(r.Next(), r.ReadUint32())
if _, ok := r.Finalise(); ok {
panic(ErrUnexpectedChecksum)
}
return a
})
}
// Dependencies returns a slice containing the backing file.
func (a *decompressArtifact) Dependencies() []Artifact {
return []Artifact{a.f}
}
// IsExclusive returns false: decompressor is fully sequential.
func (a *decompressArtifact) IsExclusive() bool { return false }
// compoundCloser is an [io.ReadCloser] with an additional [io.Closer] attached.
type compoundCloser struct {
io.ReadCloser
c io.Closer
}
// Close closes [io.ReadCloser] and the additional [io.Closer]. It returns the
// non-nil error returned by the underlying [io.ReadCloser], otherwise it
// returns the error returned by the additional [io.Closer].
func (c compoundCloser) Close() error {
err := c.ReadCloser.Close()
if _err := c.c.Close(); err == nil {
err = _err
}
return err
}
// Cure returns a decompressor [io.ReadCloser].
func (a *decompressArtifact) Cure(r *RContext) (io.ReadCloser, error) {
sr, err := r.Open(a.f)
if err != nil {
return nil, err
}
br := r.cache.getReaderRC(sr)
var dr io.ReadCloser
switch a.compress {
case Gzip:
if dr, err = gzip.NewReader(br); err != nil {
_ = br.Close()
return nil, err
}
return compoundCloser{dr, br}, nil
case Bzip2:
return struct {
io.Reader
io.Closer
}{bzip2.NewReader(br), br}, nil
default:
return nil, os.ErrInvalid
}
}
+70
View File
@@ -0,0 +1,70 @@
package pkg_test
import (
"bytes"
"compress/gzip"
"crypto/sha512"
"io/fs"
"net/http"
"testing"
"testing/fstest"
"hakurei.app/check"
"hakurei.app/internal/pkg"
)
func TestDecompress(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
if _, err := gw.Write([]byte{0}); err != nil {
t.Fatal(err)
} else if err = gw.Close(); err != nil {
t.Fatal(err)
}
testdata := buf.String()
var transport http.Transport
client := http.Client{Transport: &transport}
transport.RegisterProtocol("file", http.NewFileTransportFS(fstest.MapFS{
"testdata": {Data: []byte(testdata), Mode: 0400},
}))
testdataChecksum := func() pkg.Checksum {
h := sha512.New384()
h.Write([]byte(testdata))
return (pkg.Checksum)(h.Sum(nil))
}()
checkWithCache(t, []cacheTestCase{
{"decompress", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
cureMany(t, c, []cureStep{
{"close", pkg.NewDecompress(pkg.NewHTTPGet(
&client,
"file:///testdata",
pkg.Checksum{0xfd},
), pkg.Gzip), nil, nil, &pkg.ChecksumMismatchError{
Got: testdataChecksum,
Want: pkg.Checksum{0xfd},
}},
{"gzip", pkg.NewDecompress(pkg.NewHTTPGet(
&client,
"file:///testdata",
testdataChecksum,
), pkg.Gzip), ignorePathname, expectsChecksum(sha512.Sum384([]byte{0})), nil},
})
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/" + pkg.Encode(sha512.Sum384([]byte{0})): {Mode: 0400, Data: []byte{0}},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/QpjkahDrz7pz-tv0eAGNXR6x9NAtTjWCK5Hr7G1cIZj9rT7bLYJWUQeLD4wamAlF": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX")},
"substitute": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
})
}
-203
View File
@@ -1,203 +0,0 @@
package pkg
import (
"crypto/sha512"
"encoding/binary"
"errors"
"io"
"io/fs"
"math"
"os"
"path/filepath"
"syscall"
"hakurei.app/check"
)
// FlatEntry is a directory entry to be encoded for [Flatten].
type FlatEntry struct {
Mode fs.FileMode // file mode bits
Path string // pathname of the file
Data []byte // file content or symlink destination
}
/*
| mode uint32 | path_sz uint32 |
| data_sz uint64 |
| path string |
| data []byte |
*/
// Encode encodes the entry for transmission or hashing.
func (ent *FlatEntry) Encode(w io.Writer) (n int, err error) {
pPathSize := alignSize(len(ent.Path))
if pPathSize > math.MaxUint32 {
return 0, syscall.E2BIG
}
pDataSize := alignSize(len(ent.Data))
payload := make([]byte, wordSize*2+pPathSize+pDataSize)
binary.LittleEndian.PutUint32(payload, uint32(ent.Mode))
binary.LittleEndian.PutUint32(payload[wordSize/2:], uint32(len(ent.Path)))
binary.LittleEndian.PutUint64(payload[wordSize:], uint64(len(ent.Data)))
copy(payload[wordSize*2:], ent.Path)
copy(payload[wordSize*2+pPathSize:], ent.Data)
return w.Write(payload)
}
// ErrInsecurePath is returned by [FlatEntry.Decode] if validation is requested
// and a nonlocal path is encountered in the stream.
var ErrInsecurePath = errors.New("insecure file path")
// Decode decodes the entry from its representation produced by Encode.
func (ent *FlatEntry) Decode(r io.Reader, validate bool) (n int, err error) {
var nr int
header := make([]byte, wordSize*2)
nr, err = r.Read(header)
n += nr
if err != nil {
if errors.Is(err, io.EOF) && n != 0 {
err = io.ErrUnexpectedEOF
}
return
}
ent.Mode = fs.FileMode(binary.LittleEndian.Uint32(header))
pathSize := int(binary.LittleEndian.Uint32(header[wordSize/2:]))
pPathSize := alignSize(pathSize)
dataSize := int(binary.LittleEndian.Uint64(header[wordSize:]))
pDataSize := alignSize(dataSize)
buf := make([]byte, pPathSize+pDataSize)
nr, err = r.Read(buf)
n += nr
if err != nil {
if errors.Is(err, io.EOF) {
if nr != len(buf) {
err = io.ErrUnexpectedEOF
return
}
} else {
return
}
}
ent.Path = string(buf[:pathSize])
if ent.Mode.IsDir() {
ent.Data = nil
} else {
ent.Data = buf[pPathSize : pPathSize+dataSize]
}
if validate && !filepath.IsLocal(ent.Path) {
err = ErrInsecurePath
}
return
}
// DirScanner provides an efficient interface for reading a stream of encoded
// [FlatEntry]. Successive calls to the Scan method will step through the
// entries in the stream.
type DirScanner struct {
// Underlying reader to scan [FlatEntry] representations from.
r io.Reader
// First non-EOF I/O error, returned by the Err method.
err error
// Entry to store results in. Its address is returned by the Entry method
// and is updated on every call to Scan.
ent FlatEntry
// Validate pathnames during decoding.
validate bool
}
// NewDirScanner returns the address of a new instance of [DirScanner] reading
// from r. The caller must no longer read from r after this function returns.
func NewDirScanner(r io.Reader, validate bool) *DirScanner {
return &DirScanner{r: r, validate: validate}
}
// Err returns the first non-EOF I/O error.
func (s *DirScanner) Err() error {
if errors.Is(s.err, io.EOF) {
return nil
}
return s.err
}
// Entry returns the address to the [FlatEntry] value storing the last result.
func (s *DirScanner) Entry() *FlatEntry { return &s.ent }
// Scan advances to the next [FlatEntry].
func (s *DirScanner) Scan() bool {
if s.err != nil {
return false
}
var n int
n, s.err = s.ent.Decode(s.r, s.validate)
if errors.Is(s.err, io.EOF) {
return n != 0
}
return s.err == nil
}
// Flatten writes a deterministic representation of the contents of fsys to w.
// The resulting data can be hashed to produce a deterministic checksum for the
// directory.
func Flatten(fsys fs.FS, root string, w io.Writer) (n int, err error) {
var nr int
err = fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
var fi fs.FileInfo
fi, err = d.Info()
if err != nil {
return err
}
ent := FlatEntry{
Path: path,
Mode: fi.Mode(),
}
if ent.Mode.IsRegular() {
if ent.Data, err = fs.ReadFile(fsys, path); err != nil {
return err
}
} else if ent.Mode&fs.ModeSymlink != 0 {
var newpath string
if newpath, err = fs.ReadLink(fsys, path); err != nil {
return err
}
ent.Data = []byte(newpath)
} else if !ent.Mode.IsDir() {
return InvalidFileModeError(ent.Mode)
}
nr, err = ent.Encode(w)
n += nr
return err
})
return
}
// HashFS returns a checksum produced by hashing the result of [Flatten].
func HashFS(buf *Checksum, fsys fs.FS, root string) error {
h := sha512.New384()
if _, err := Flatten(fsys, root, h); err != nil {
return err
}
h.Sum(buf[:0])
return nil
}
// HashDir returns a checksum produced by hashing the result of [Flatten].
func HashDir(buf *Checksum, pathname *check.Absolute) error {
return HashFS(buf, os.DirFS(pathname.String()), ".")
}
-621
View File
@@ -1,621 +0,0 @@
package pkg_test
import (
"bytes"
"io/fs"
"reflect"
"testing"
"testing/fstest"
"hakurei.app/internal/pkg"
)
func TestFlatten(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
fsys fs.FS
entries []pkg.FlatEntry
sum pkg.Checksum
err error
}{
{"bad type", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"invalid": {Mode: fs.ModeCharDevice | 0400},
}, nil, pkg.Checksum{}, pkg.InvalidFileModeError(
fs.ModeCharDevice | 0400,
)},
{"coldboot", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"devices": {Mode: fs.ModeDir | 0700},
"devices/uevent": {Mode: 0600, Data: []byte("add")},
"devices/empty": {Mode: fs.ModeDir | 0700},
"devices/sub": {Mode: fs.ModeDir | 0700},
"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"block": {Mode: fs.ModeDir | 0700},
"block/uevent": {Mode: 0600, Data: []byte{}},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "block"},
{Mode: 0600, Path: "block/uevent", Data: []byte{}},
{Mode: fs.ModeDir | 0700, Path: "devices"},
{Mode: fs.ModeDir | 0700, Path: "devices/empty"},
{Mode: fs.ModeDir | 0700, Path: "devices/sub"},
{Mode: 0600, Path: "devices/sub/uevent", Data: []byte("add")},
{Mode: 0600, Path: "devices/uevent", Data: []byte("add")},
}, pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8"), nil},
{"empty", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"identifier": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C"), nil},
{"sample cache file", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX": {Mode: 0400, Data: []byte{0}},
"checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq": {Mode: 0400, Data: []byte{0, 0, 0, 0, 0xad, 0xb, 0, 4, 0xfe, 0xfe, 0, 0, 0xfe, 0xca, 0, 0}},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX")},
"identifier/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq")},
"identifier/cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq")},
"identifier/deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq")},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: 0400, Path: "checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq", Data: []byte{0, 0, 0, 0, 0xad, 0xb, 0, 4, 0xfe, 0xfe, 0, 0, 0xfe, 0xca, 0, 0}},
{Mode: 0400, Path: "checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX", Data: []byte{0}},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq", Data: []byte("../checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe", Data: []byte("../checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", Data: []byte("../checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX", Data: []byte("../checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX")},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("St9rlE-mGZ5gXwiv_hzQ_B8bZP-UUvSNmf4nHUZzCMOumb6hKnheZSe0dmnuc4Q2"), nil},
{"sample http get cure", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/fLYGIMHgN1louE-JzITJZJo2SDniPu-IHBXubtvQWFO-hXnDVKNuscV7-zlyr5fU": {Mode: 0400, Data: []byte("\x7f\xe1\x69\xa2\xdd\x63\x96\x26\x83\x79\x61\x8b\xf0\x3f\xd5\x16\x9a\x39\x3a\xdb\xcf\xb1\xbc\x8d\x33\xff\x75\xee\x62\x56\xa9\xf0\x27\xac\x13\x94\x69")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/oM-2pUlk-mOxK1t3aMWZer69UdOQlAXiAgMrpZ1476VoOqpYVP1aGFS9_HYy-D8_": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fLYGIMHgN1louE-JzITJZJo2SDniPu-IHBXubtvQWFO-hXnDVKNuscV7-zlyr5fU")},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: 0400, Path: "checksum/fLYGIMHgN1louE-JzITJZJo2SDniPu-IHBXubtvQWFO-hXnDVKNuscV7-zlyr5fU", Data: []byte("\x7f\xe1\x69\xa2\xdd\x63\x96\x26\x83\x79\x61\x8b\xf0\x3f\xd5\x16\x9a\x39\x3a\xdb\xcf\xb1\xbc\x8d\x33\xff\x75\xee\x62\x56\xa9\xf0\x27\xac\x13\x94\x69")},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/oM-2pUlk-mOxK1t3aMWZer69UdOQlAXiAgMrpZ1476VoOqpYVP1aGFS9_HYy-D8_", Data: []byte("../checksum/fLYGIMHgN1louE-JzITJZJo2SDniPu-IHBXubtvQWFO-hXnDVKNuscV7-zlyr5fU")},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("L_0RFHpr9JUS4Zp14rz2dESSRvfLzpvqsLhR1-YjQt8hYlmEdVl7vI3_-v8UNPKs"), nil},
{"sample directory step simple", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500},
"check": {Mode: 0400, Data: []byte{0, 0}},
"lib": {Mode: fs.ModeDir | 0700},
"lib/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
"lib/pkgconfig": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0500, Path: "."},
{Mode: 0400, Path: "check", Data: []byte{0, 0}},
{Mode: fs.ModeDir | 0700, Path: "lib"},
{Mode: fs.ModeSymlink | 0777, Path: "lib/libedac.so", Data: []byte("/proc/nonexistent/libedac.so")},
{Mode: fs.ModeDir | 0700, Path: "lib/pkgconfig"},
}, pkg.MustDecode("qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b"), nil},
{"sample directory step garbage", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500},
"lib": {Mode: fs.ModeDir | 0500},
"lib/check": {Mode: 0400, Data: []byte{}},
"lib/pkgconfig": {Mode: fs.ModeDir | 0500},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0500, Path: "."},
{Mode: fs.ModeDir | 0500, Path: "lib"},
{Mode: 0400, Path: "lib/check", Data: []byte{}},
{Mode: fs.ModeDir | 0500, Path: "lib/pkgconfig"},
}, pkg.MustDecode("CUx-3hSbTWPsbMfDhgalG4Ni_GmR9TnVX8F99tY_P5GtkYvczg9RrF5zO0jX9XYT"), nil},
{"sample directory", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b": {Mode: fs.ModeDir | 0500},
"checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b/check": {Mode: 0400, Data: []byte{0, 0}},
"checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b/lib": {Mode: fs.ModeDir | 0700},
"checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b/lib/pkgconfig": {Mode: fs.ModeDir | 0700},
"checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b/lib/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b")},
"identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b")},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b"},
{Mode: 0400, Path: "checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b/check", Data: []byte{0, 0}},
{Mode: fs.ModeDir | 0700, Path: "checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b/lib"},
{Mode: fs.ModeSymlink | 0777, Path: "checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b/lib/libedac.so", Data: []byte("/proc/nonexistent/libedac.so")},
{Mode: fs.ModeDir | 0700, Path: "checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b/lib/pkgconfig"},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY", Data: []byte("../checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa", Data: []byte("../checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b")},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("WVpvsVqVKg9Nsh744x57h51AuWUoUR2nnh8Md-EYBQpk6ziyTuUn6PLtF2e0Eu_d"), nil},
{"sample no assume checksum", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M": {Mode: fs.ModeDir | 0500},
"checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M/check": {Mode: 0400, Data: []byte{}},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/_wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
"identifier/_wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M"},
{Mode: 0400, Path: "checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M/check", Data: []byte{}},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/_wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/_wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("OC290t23aimNo2Rp2pPwan5GI2KRLRdOwYxXQMD9jw0QROgHnNXWodoWdV0hwu2w"), nil},
{"sample tar step unpack", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500},
"checksum": {Mode: fs.ModeDir | 0500},
"checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP": {Mode: fs.ModeDir | 0500},
"checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check": {Mode: 0400, Data: []byte{0, 0}},
"checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib": {Mode: fs.ModeDir | 0500},
"checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig": {Mode: fs.ModeDir | 0500},
"checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
"identifier": {Mode: fs.ModeDir | 0500},
"identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
"identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
"work": {Mode: fs.ModeDir | 0500},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0500, Path: "."},
{Mode: fs.ModeDir | 0500, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP"},
{Mode: 0400, Path: "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check", Data: []byte{0, 0}},
{Mode: fs.ModeDir | 0500, Path: "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib"},
{Mode: fs.ModeSymlink | 0777, Path: "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so", Data: []byte("/proc/nonexistent/libedac.so")},
{Mode: fs.ModeDir | 0500, Path: "checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig"},
{Mode: fs.ModeDir | 0500, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY", Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa", Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
{Mode: fs.ModeDir | 0500, Path: "work"},
}, pkg.MustDecode("cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM"), nil},
{"sample tar", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM": {Mode: fs.ModeDir | 0500},
"checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/checksum": {Mode: fs.ModeDir | 0500},
"checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP": {Mode: fs.ModeDir | 0500},
"checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check": {Mode: 0400, Data: []byte{0, 0}},
"checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib": {Mode: fs.ModeDir | 0500},
"checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
"checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig": {Mode: fs.ModeDir | 0500},
"checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/identifier": {Mode: fs.ModeDir | 0500},
"checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
"checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
"checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/work": {Mode: fs.ModeDir | 0500},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/W5S65DEhawz_WKaok5NjUKLmnD9dNl5RPauNJjcOVcB3VM4eGhSaLGmXbL8vZpiw": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM")},
"identifier/rg7F1D5hwv6o4xctjD5zDq4i5MD0mArTsUIWfhUbik8xC6Bsyt3mjXXOm3goojTz": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM")},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM"},
{Mode: fs.ModeDir | 0500, Path: "checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP"},
{Mode: 0400, Path: "checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check", Data: []byte{0, 0}},
{Mode: fs.ModeDir | 0500, Path: "checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib"},
{Mode: fs.ModeSymlink | 0777, Path: "checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so", Data: []byte("/proc/nonexistent/libedac.so")},
{Mode: fs.ModeDir | 0500, Path: "checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig"},
{Mode: fs.ModeDir | 0500, Path: "checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY", Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
{Mode: fs.ModeSymlink | 0777, Path: "checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa", Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
{Mode: fs.ModeDir | 0500, Path: "checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM/work"},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/W5S65DEhawz_WKaok5NjUKLmnD9dNl5RPauNJjcOVcB3VM4eGhSaLGmXbL8vZpiw", Data: []byte("../checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/rg7F1D5hwv6o4xctjD5zDq4i5MD0mArTsUIWfhUbik8xC6Bsyt3mjXXOm3goojTz", Data: []byte("../checksum/cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM")},
{Mode: fs.ModeDir | 0700, Path: "temp"},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("NQTlc466JmSVLIyWklm_u8_g95jEEb98PxJU-kjwxLpfdjwMWJq0G8ze9R4Vo1Vu"), nil},
{"sample tar expand step unpack", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500},
"libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0500, Path: "."},
{Mode: fs.ModeSymlink | 0777, Path: "libedac.so", Data: []byte("/proc/nonexistent/libedac.so")},
}, pkg.MustDecode("CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN"), nil},
{"sample tar expand", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN": {Mode: fs.ModeDir | 0500},
"checksum/CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/W5S65DEhawz_WKaok5NjUKLmnD9dNl5RPauNJjcOVcB3VM4eGhSaLGmXbL8vZpiw": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN")},
"identifier/_v1blm2h-_KA-dVaawdpLas6MjHc6rbhhFS8JWwx8iJxZGUu8EBbRrhr5AaZ9PJL": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN")},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN"},
{Mode: fs.ModeSymlink | 0777, Path: "checksum/CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN/libedac.so", Data: []byte("/proc/nonexistent/libedac.so")},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/W5S65DEhawz_WKaok5NjUKLmnD9dNl5RPauNJjcOVcB3VM4eGhSaLGmXbL8vZpiw", Data: []byte("../checksum/CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/_v1blm2h-_KA-dVaawdpLas6MjHc6rbhhFS8JWwx8iJxZGUu8EBbRrhr5AaZ9PJL", Data: []byte("../checksum/CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN")},
{Mode: fs.ModeDir | 0700, Path: "temp"},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("hSoSSgCYTNonX3Q8FjvjD1fBl-E-BQyA6OTXro2OadXqbST4tZ-akGXszdeqphRe"), nil},
{"testtool", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500},
"check": {Mode: 0400, Data: []byte{0}},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0500, Path: "."},
{Mode: 0400, Path: "check", Data: []byte{0}},
}, pkg.MustDecode("GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9"), nil},
{"sample exec container", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9": {Mode: fs.ModeDir | 0500},
"checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check": {Mode: 0400, Data: []byte{0}},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400, Data: []byte{}},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
"identifier/dztPS6jRjiZtCF4_p8AzfnxGp6obkhrgFVsxdodbKWUoAEVtDz3MykepJB4kI_ks": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9"},
{Mode: 0400, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check", Data: []byte{0}},
{Mode: fs.ModeDir | 0500, Path: "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU"},
{Mode: 0400, Path: "checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb", Data: []byte{}},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/dztPS6jRjiZtCF4_p8AzfnxGp6obkhrgFVsxdodbKWUoAEVtDz3MykepJB4kI_ks", Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK", Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
{Mode: fs.ModeDir | 0700, Path: "temp"},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("Q5DluWQCAeohLoiGRImurwFp3vdz9IfQCoj7Fuhh73s4KQPRHpEQEnHTdNHmB8Fx"), nil},
{"testtool net", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500},
"check": {Mode: 0400, Data: []byte("net")},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0500, Path: "."},
{Mode: 0400, Path: "check", Data: []byte("net")},
}, pkg.MustDecode("a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W"), nil},
{"sample exec net container", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400, Data: []byte{}},
"checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W": {Mode: fs.ModeDir | 0500},
"checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W/check": {Mode: 0400, Data: []byte("net")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/G8qPxD9puvvoOVV7lrT80eyDeIl3G_CCFoKw12c8mCjMdG1zF7NEPkwYpNubClK3": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W")},
"identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU"},
{Mode: 0400, Path: "checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb", Data: []byte{}},
{Mode: fs.ModeDir | 0500, Path: "checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W"},
{Mode: 0400, Path: "checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W/check", Data: []byte("net")},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/G8qPxD9puvvoOVV7lrT80eyDeIl3G_CCFoKw12c8mCjMdG1zF7NEPkwYpNubClK3", Data: []byte("../checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK", Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
{Mode: fs.ModeDir | 0700, Path: "temp"},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("bPYvvqxpfV7xcC1EptqyKNK1klLJgYHMDkzBcoOyK6j_Aj5hb0mXNPwTwPSK5F6Z"), nil},
{"sample exec container overlay root", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9": {Mode: fs.ModeDir | 0500},
"checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check": {Mode: 0400, Data: []byte{0}},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/RdMA-mubnrHuu3Ky1wWyxauSYCO0ZH_zCPUj3uDHqkfwv5sGcByoF_g5PjlGiClb": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9"},
{Mode: 0400, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check", Data: []byte{0}},
{Mode: fs.ModeDir | 0500, Path: "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU"},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/RdMA-mubnrHuu3Ky1wWyxauSYCO0ZH_zCPUj3uDHqkfwv5sGcByoF_g5PjlGiClb", Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK", Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
{Mode: fs.ModeDir | 0700, Path: "temp"},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("PO2DSSCa4yoSgEYRcCSZfQfwow1yRigL3Ry-hI0RDI4aGuFBha-EfXeSJnG_5_Rl"), nil},
{"sample exec container overlay work", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9": {Mode: fs.ModeDir | 0500},
"checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check": {Mode: 0400, Data: []byte{0}},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/5hlaukCirnXE4W_RSLJFOZN47Z5RiHnacXzdFp_70cLgiJUGR6cSb_HaFftkzi0-": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9"},
{Mode: 0400, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check", Data: []byte{0}},
{Mode: fs.ModeDir | 0500, Path: "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU"},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/5hlaukCirnXE4W_RSLJFOZN47Z5RiHnacXzdFp_70cLgiJUGR6cSb_HaFftkzi0-", Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK", Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
{Mode: fs.ModeDir | 0700, Path: "temp"},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("iaRt6l_Wm2n-h5UsDewZxQkCmjZjyL8r7wv32QT2kyV55-Lx09Dq4gfg9BiwPnKs"), nil},
{"sample exec container multiple layers", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9": {Mode: fs.ModeDir | 0500},
"checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check": {Mode: 0400, Data: []byte{0}},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400, Data: []byte{}},
"checksum/nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK": {Mode: fs.ModeDir | 0500},
"checksum/nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK/check": {Mode: 0400, Data: []byte("layers")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
"identifier/B-kc5iJMx8GtlCua4dz6BiJHnDAOUfPjgpbKq4e-QEn0_CZkSYs3fOA1ve06qMs2": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK")},
"identifier/p1t_drXr34i-jZNuxDMLaMOdL6tZvQqhavNafGynGqxOZoXAUTSn7kqNh3Ovv3DT": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9"},
{Mode: 0400, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check", Data: []byte{0}},
{Mode: fs.ModeDir | 0500, Path: "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU"},
{Mode: 0400, Path: "checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb", Data: []byte{}},
{Mode: fs.ModeDir | 0500, Path: "checksum/nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK"},
{Mode: 0400, Path: "checksum/nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK/check", Data: []byte("layers")},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/B-kc5iJMx8GtlCua4dz6BiJHnDAOUfPjgpbKq4e-QEn0_CZkSYs3fOA1ve06qMs2", Data: []byte("../checksum/nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/p1t_drXr34i-jZNuxDMLaMOdL6tZvQqhavNafGynGqxOZoXAUTSn7kqNh3Ovv3DT", Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK", Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
{Mode: fs.ModeDir | 0700, Path: "temp"},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("O2YzyR7IUGU5J2CADy0hUZ3A5NkP_Vwzs4UadEdn2oMZZVWRtH0xZGJ3HXiimTnZ"), nil},
{"sample exec container layer promotion", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9": {Mode: fs.ModeDir | 0500},
"checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check": {Mode: 0400, Data: []byte{0}},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/kvJIqZo5DKFOxC2ZQ-8_nPaQzEAz9cIm3p6guO-uLqm-xaiPu7oRkSnsu411jd_U": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"identifier/xXTIYcXmgJWNLC91c417RRrNM9cjELwEZHpGvf8Fk_GNP5agRJp_SicD0w9aMeLJ": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9"},
{Mode: 0400, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check", Data: []byte{0}},
{Mode: fs.ModeDir | 0500, Path: "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU"},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/kvJIqZo5DKFOxC2ZQ-8_nPaQzEAz9cIm3p6guO-uLqm-xaiPu7oRkSnsu411jd_U", Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK", Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/xXTIYcXmgJWNLC91c417RRrNM9cjELwEZHpGvf8Fk_GNP5agRJp_SicD0w9aMeLJ", Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")},
{Mode: fs.ModeDir | 0700, Path: "temp"},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("3EaW6WibLi9gl03_UieiFPaFcPy5p4x3JPxrnLJxGaTI-bh3HU9DK9IMx7c3rrNm"), nil},
{"sample file short", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX": {Mode: 0400, Data: []byte{0}},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/3376ALA7hIUm2LbzH2fDvRezgzod1eTK_G6XjyOgbM2u-6swvkFaF0BOwSl_juBi": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX")},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: 0400, Path: "checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX", Data: []byte{0}},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/3376ALA7hIUm2LbzH2fDvRezgzod1eTK_G6XjyOgbM2u-6swvkFaF0BOwSl_juBi", Data: []byte("../checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX")},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("iR6H5OIsyOW4EwEgtm9rGzGF6DVtyHLySEtwnFE8bnus9VJcoCbR4JIek7Lw-vwT"), nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("roundtrip", func(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
if _, err := pkg.Flatten(
tc.fsys,
".",
&buf,
); !reflect.DeepEqual(err, tc.err) {
t.Fatalf("Flatten: error = %v, want %v", err, tc.err)
} else if tc.err != nil {
return
}
s := pkg.NewDirScanner(bytes.NewReader(buf.Bytes()), true)
var got []pkg.FlatEntry
for s.Scan() {
got = append(got, *s.Entry())
}
if err := s.Err(); err != nil {
t.Fatalf("Err: error = %v", err)
}
if !reflect.DeepEqual(got, tc.entries) {
t.Fatalf("Scan: %#v, want %#v", got, tc.entries)
}
})
if tc.err != nil {
return
}
t.Run("hash", func(t *testing.T) {
t.Parallel()
var got pkg.Checksum
if err := pkg.HashFS(&got, tc.fsys, "."); err != nil {
t.Fatalf("HashFS: error = %v", err)
} else if got != tc.sum {
t.Fatalf("HashFS: %v", &pkg.ChecksumMismatchError{
Got: got,
Want: tc.sum,
})
}
})
})
}
}
+119 -35
View File
@@ -6,11 +6,14 @@ import (
"errors"
"fmt"
"io"
"maps"
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"strconv"
"sync"
"syscall"
"time"
"unique"
@@ -94,6 +97,41 @@ func MustPath(pathname string, writable bool, a ...Artifact) ExecPath {
return ExecPath{check.MustAbs(pathname), a, writable}
}
var (
binfmt map[string]container.BinfmtEntry
binfmtMu sync.RWMutex
)
// RegisterArch arranges for [KindExec] and [KindExecNet] to support a new
// architecture via a binfmt_misc entry. Each architecture must be registered
// at most once.
func RegisterArch(arch string, e container.BinfmtEntry) {
if arch == "" {
panic(UnsupportedArchError(arch))
}
binfmtMu.Lock()
defer binfmtMu.Unlock()
if binfmt == nil {
binfmt = make(map[string]container.BinfmtEntry)
}
if _, ok := binfmt[arch]; ok {
panic("attempting to register " + strconv.Quote(arch) + " twice")
}
binfmt[arch] = e
}
// Arch returns a snapshot of currently registered [KindExec] and [KindExecNet]
// binfmt entries.
func Arch() map[string]container.BinfmtEntry {
binfmtMu.RLock()
r := maps.Clone(binfmt)
binfmtMu.RUnlock()
return r
}
const (
// ExecTimeoutDefault replaces out of range [NewExec] timeout values.
ExecTimeoutDefault = 15 * time.Minute
@@ -110,6 +148,8 @@ type execArtifact struct {
// Caller-supplied user-facing reporting name, guaranteed to be nonzero
// during initialisation.
name string
// Target architecture.
arch string
// Caller-supplied inner mount points.
paths []ExecPath
@@ -132,28 +172,40 @@ type execArtifact struct {
var _ fmt.Stringer = new(execArtifact)
// execNetArtifact is like execArtifact but implements [KnownChecksum] and has
// its resulting container keep the host net namespace.
type execNetArtifact struct {
// execMeasuredArtifact is like execArtifact but implements [KnownChecksum] and
// has its resulting container optionally keep the host net namespace.
type execMeasuredArtifact struct {
checksum Checksum
// Whether to keep host net namespace.
hostNet bool
execArtifact
}
var _ KnownChecksum = new(execNetArtifact)
var _ KnownChecksum = new(execMeasuredArtifact)
// Checksum returns the caller-supplied checksum.
func (a *execNetArtifact) Checksum() Checksum { return a.checksum }
func (a *execMeasuredArtifact) Checksum() Checksum { return a.checksum }
// Kind returns the hardcoded [Kind] constant.
func (*execNetArtifact) Kind() Kind { return KindExecNet }
// Kind returns [KindExecNet], or [KindExec] if hostNet is false.
func (a *execMeasuredArtifact) Kind() Kind {
if a == nil || a.hostNet {
return KindExecNet
}
return KindExec
}
// Cure cures the [Artifact] in the container described by the caller. The
// container retains host networking.
func (a *execNetArtifact) Cure(f *FContext) error {
return a.cure(f, true)
// container optionally retains host networking.
func (a *execMeasuredArtifact) Cure(f *FContext) error {
return a.cure(f, a.hostNet)
}
// ErrNetChecksum is panicked by [NewExec] if host net namespace is requested
// with a nil checksum.
var ErrNetChecksum = errors.New("attempting to keep net namespace without checksum")
// NewExec returns a new [Artifact] that executes the program path in a
// container with specified paths bind mounted read-only in order. A private
// instance of /proc and /dev is made available to the container.
@@ -167,7 +219,7 @@ func (a *execNetArtifact) Cure(f *FContext) error {
// regular or symlink.
//
// If checksum is non-nil, the resulting [Artifact] implements [KnownChecksum]
// and its container runs in the host net namespace.
// and its container optionally runs in the host net namespace.
//
// The container is allowed to run for the specified duration before the initial
// process and all processes originating from it is terminated. A zero or
@@ -178,10 +230,10 @@ func (a *execNetArtifact) Cure(f *FContext) error {
// container and does not affect curing outcome. Because of this, it is omitted
// from parameter data for computing identifier.
func NewExec(
name string,
name, arch string,
checksum *Checksum,
timeout time.Duration,
exclusive bool,
hostNet, exclusive bool,
dir *check.Absolute,
env []string,
@@ -193,17 +245,23 @@ func NewExec(
if name == "" {
name = "exec-" + filepath.Base(pathname.String())
}
if arch == "" {
arch = runtime.GOARCH
}
if timeout <= 0 {
timeout = ExecTimeoutDefault
}
if timeout > ExecTimeoutMax {
timeout = ExecTimeoutMax
}
a := execArtifact{name, paths, dir, env, pathname, args, timeout, exclusive}
a := execArtifact{name, arch, paths, dir, env, pathname, args, timeout, exclusive}
if checksum == nil {
if hostNet {
panic(ErrNetChecksum)
}
return &a
}
return &execNetArtifact{*checksum, a}
return &execMeasuredArtifact{*checksum, hostNet, a}
}
// Kind returns the hardcoded [Kind] constant.
@@ -211,6 +269,7 @@ func (*execArtifact) Kind() Kind { return KindExec }
// Params writes paths, executable pathname and args.
func (a *execArtifact) Params(ctx *IContext) {
ctx.WriteString(a.arch)
ctx.WriteString(a.name)
ctx.WriteUint32(uint32(len(a.paths)))
@@ -257,11 +316,26 @@ func (a *execArtifact) Params(ctx *IContext) {
}
}
// UnsupportedArchError describes an unsupported or invalid architecture.
type UnsupportedArchError string
func (e UnsupportedArchError) Error() string {
if e == "" {
return "invalid architecture name"
}
return "unsupported architecture " + string(e)
}
// readExecArtifact interprets IR values and returns the address of execArtifact
// or execNetArtifact.
func readExecArtifact(r *IRReader, net bool) Artifact {
r.DiscardAll()
arch := r.ReadString()
if arch == "" {
panic(UnsupportedArchError(arch))
}
name := r.ReadString()
sz := r.ReadUint32()
@@ -312,22 +386,17 @@ func readExecArtifact(r *IRReader, net bool) Artifact {
exclusive := r.ReadUint32() != 0
checksum, ok := r.Finalise()
var checksumP *Checksum
if net {
if !ok {
panic(ErrExpectedChecksum)
}
checksumVal := checksum.Value()
checksumP = &checksumVal
} else {
if ok {
panic(ErrUnexpectedChecksum)
}
if ok {
checksumP = new(checksum.Value())
}
if net && !ok {
panic(ErrExpectedChecksum)
}
return NewExec(
name, checksumP, timeout, exclusive, dir, env, pathname, args, paths...,
name, arch, checksumP, timeout, net, exclusive, dir, env, pathname, args, paths...,
)
}
@@ -436,11 +505,23 @@ func (a *execArtifact) makeContainer(
if z.HostNet {
z.Hostname = "cure-net"
}
z.Quiet = flags&CSuppressInit != 0
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
z.Dir, z.Path, z.Args = a.dir, a.path, a.args
z.Env = slices.Concat(a.env, []string{EnvJobs + "=" + strconv.Itoa(jobs)})
z.Grow(len(a.paths) + 4)
if a.arch != runtime.GOARCH {
binfmtMu.RLock()
e, ok := binfmt[a.arch]
binfmtMu.RUnlock()
if !ok {
return nil, UnsupportedArchError(a.arch)
}
z.Binfmt = []container.BinfmtEntry{e}
z.InitAsRoot = true
}
for i, b := range a.paths {
if i == overlayWorkIndex {
if err = os.MkdirAll(work.String(), 0700); err != nil {
@@ -510,6 +591,7 @@ var (
func (c *Cache) EnterExec(
ctx context.Context,
a Artifact,
hostname string,
retainSession bool,
stdin io.Reader,
stdout, stderr io.Writer,
@@ -527,9 +609,9 @@ func (c *Cache) EnterExec(
case *execArtifact:
e = f
case *execNetArtifact:
case *execMeasuredArtifact:
e = &f.execArtifact
hostNet = true
hostNet = f.hostNet
default:
return ErrNotExec
@@ -590,6 +672,9 @@ func (c *Cache) EnterExec(
z.Env = append(z.Env, "TERM="+s)
}
}
if hostname != "" {
z.Hostname = hostname
}
if err = z.Start(); err != nil {
return
@@ -630,12 +715,6 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
_ = stdout.Close()
return
}
defer func() {
if err != nil && !errors.As(err, new(*exec.ExitError)) {
_ = stdout.Close()
_ = stderr.Close()
}
}()
brStdout, brStderr := f.cache.getReader(stdout), f.cache.getReader(stderr)
stdoutDone, stderrDone := make(chan struct{}), make(chan struct{})
@@ -650,6 +729,11 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
io.TeeReader(brStderr, status),
)
defer func() {
if err != nil && !errors.As(err, new(*exec.ExitError)) {
_ = stdout.Close()
_ = stderr.Close()
}
<-stdoutDone
<-stderrDone
f.cache.putReader(brStdout)
+297 -50
View File
@@ -1,44 +1,70 @@
package pkg_test
//go:generate env CGO_ENABLED=0 go build -tags testtool -o testdata/testtool ./testdata
import (
"bytes"
_ "embed"
"encoding/gob"
"errors"
"io/fs"
"net"
"os"
"os/exec"
"path/filepath"
"slices"
"testing"
"unique"
"hakurei.app/check"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/info"
"hakurei.app/internal/pkg"
"hakurei.app/internal/stub"
"hakurei.app/internal/pkg/internal/testtool/expected"
)
// testtoolBin is the container test tool binary made available to the
// execArtifact for testing its curing environment.
//
//go:embed testdata/testtool
//go:generate env CGO_ENABLED=0 go build -tags testtool -o internal/testtool ./internal/testtool
//go:embed internal/testtool/testtool
var testtoolBin []byte
func init() {
pathname, err := filepath.Abs("internal/testtool/testtool")
if err != nil {
panic(err)
}
pkg.RegisterArch("cafe", container.BinfmtEntry{
Magic: expected.Magic,
Interpreter: check.MustAbs(pathname),
})
}
func TestExec(t *testing.T) {
t.Parallel()
wantChecksumOffline := pkg.MustDecode(
"GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9",
)
wantOffline := expectsFS{
".": {Mode: fs.ModeDir | 0500},
"check": {Mode: 0400, Data: []byte{0}},
}
wantOfflineEncode := pkg.Encode(wantOffline.hash())
failingArtifact := &stubArtifact{
kind: pkg.KindTar,
params: []byte("doomed artifact"),
cure: func(t *pkg.TContext) error {
return stub.UniqueError(0xcafe)
},
}
checkWithCache(t, []cacheTestCase{
{"offline", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"offline", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{
{"container", pkg.NewExec(
"exec-offline", nil, 0, false,
"exec-offline", "", new(wantOffline.hash()), 0, false, false,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1"},
check.MustAbs("/opt/bin/testtool"),
@@ -58,67 +84,128 @@ func TestExec(t *testing.T) {
},
}),
pkg.MustPath("/opt", false, testtool),
), ignorePathname, wantChecksumOffline, nil},
), ignorePathname, wantOffline, nil},
{"error passthrough", pkg.NewExec(
"", nil, 0, true,
{"substitution", pkg.NewExec(
"exec-offline", "", new(wantOffline.hash()), 0, false, false,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1"},
check.MustAbs("/opt/bin/testtool"),
[]string{"testtool"},
pkg.MustPath("/proc/nonexistent", false, &stubArtifact{
pkg.MustPath("/file", false, newStubFile(
pkg.KindHTTPGet,
pkg.ID{0xfe, 0},
nil,
nil, nil,
)),
// substitution miss fails in testtool due to differing idents
pkg.MustPath("/.hakurei", false, &stubArtifact{
kind: pkg.KindTar,
params: []byte("doomed artifact"),
params: []byte("empty directory (substituted)"),
cure: func(t *pkg.TContext) error {
return stub.UniqueError(0xcafe)
return os.MkdirAll(t.GetWorkDir().String(), 0700)
},
}),
), nil, pkg.Checksum{}, &pkg.DependencyCureError{
pkg.MustPath("/opt", false, testtool),
), ignorePathname, wantOffline, nil},
{"error passthrough", pkg.NewExec(
"", "", nil, 0, false, true,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1"},
check.MustAbs("/opt/bin/testtool"),
[]string{"testtool"},
pkg.MustPath("/proc/nonexistent", false, failingArtifact),
), nil, nil, &pkg.DependencyCureError{
{
Ident: unique.Make(pkg.ID(pkg.MustDecode(
"Sowo6oZRmG6xVtUaxB6bDWZhVsqAJsIJWUp0OPKlE103cY0lodx7dem8J-qQF0Z1",
))),
A: failingArtifact,
Err: stub.UniqueError(0xcafe),
},
}},
{"invalid paths", pkg.NewExec(
"", nil, 0, false,
"", "", nil, 0, false, false,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1"},
check.MustAbs("/opt/bin/testtool"),
[]string{"testtool"},
pkg.ExecPath{},
), nil, pkg.Checksum{}, pkg.ErrInvalidPaths},
), nil, nil, pkg.ErrInvalidPaths},
})
// check init failure passthrough
var exitError *exec.ExitError
if _, _, err := c.Cure(pkg.NewExec(
"", nil, 0, false,
initFailureArtifact := pkg.NewExec(
"", "", nil, 0, false, false,
pkg.AbsWork,
nil,
check.MustAbs("/opt/bin/testtool"),
[]string{"testtool"},
)); !errors.As(err, &exitError) ||
)
var exitError *exec.ExitError
if _, _, err := c.Cure(initFailureArtifact); !errors.As(err, &exitError) ||
exitError.ExitCode() != hst.ExitFailure {
t.Fatalf("Cure: error = %v, want init exit status 1", err)
}
testtoolDestroy(t, base, c)
}, pkg.MustDecode("Q5DluWQCAeohLoiGRImurwFp3vdz9IfQCoj7Fuhh73s4KQPRHpEQEnHTdNHmB8Fx")},
var faultStatus []byte
if faults, err := c.ReadFaults(initFailureArtifact); err != nil {
t.Fatal(err)
} else if len(faults) != 1 {
t.Fatalf("ReadFaults: %v", faults)
} else if faultStatus, err = os.ReadFile(faults[0].String()); err != nil {
t.Fatal(err)
} else if err = faults[0].Destroy(); err != nil {
t.Fatal(err)
} else {
t.Logf("destroyed expected fault at %s", faults[0].Time().UTC())
}
{"net", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
if !bytes.HasPrefix(faultStatus, []byte(
"internal/pkg ",
)) || !bytes.Contains(faultStatus, []byte(
"\ninit: fork/exec /opt/bin/testtool: no such file or directory\n",
)) {
t.Errorf("unexpected status:\n%s", string(faultStatus))
}
destroyStatus(t, base, 2, 1)
testtoolDestroy(t, base, c)
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/" + wantOfflineEncode: {Mode: fs.ModeDir | 0500},
"checksum/" + wantOfflineEncode + "/check": {Mode: 0400, Data: []byte{0}},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400, Data: []byte{}},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/QwS7SmiatdqryQYgESdGw7Yw2PcpNf0vNfpvUA0t92BTlKiUjfCrXyMW17G2X77X": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
"identifier/" + expected.Offline: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)},
"identifier/" + expected.OfflineS: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"substitute": {Mode: fs.ModeDir | 0700},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
{"net", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
testtool, testtoolDestroy := newTesttool()
wantChecksum := pkg.MustDecode(
"a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W",
)
wantNet := expectsFS{
".": {Mode: fs.ModeDir | 0500},
"check": {Mode: 0400, Data: []byte("net")},
}
cureMany(t, c, []cureStep{
{"container", pkg.NewExec(
"exec-net", &wantChecksum, 0, false,
"exec-net", "", new(wantNet.hash()), 0, true, false,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1"},
check.MustAbs("/opt/bin/testtool"),
@@ -138,18 +225,37 @@ func TestExec(t *testing.T) {
},
}),
pkg.MustPath("/opt", false, testtool),
), ignorePathname, wantChecksum, nil},
), ignorePathname, wantNet, nil},
})
destroyStatus(t, base, 2, 0)
testtoolDestroy(t, base, c)
}, pkg.MustDecode("bPYvvqxpfV7xcC1EptqyKNK1klLJgYHMDkzBcoOyK6j_Aj5hb0mXNPwTwPSK5F6Z")},
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
{"overlay root", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400, Data: []byte{}},
"checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W": {Mode: fs.ModeDir | 0500},
"checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W/check": {Mode: 0400, Data: []byte("net")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/" + expected.Net: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W")},
"identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"substitute": {Mode: fs.ModeDir | 0700},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
{"overlay root", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{
{"container", pkg.NewExec(
"exec-overlay-root", nil, 0, false,
"exec-overlay-root", "", nil, 0, false, false,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"},
check.MustAbs("/opt/bin/testtool"),
@@ -163,18 +269,35 @@ func TestExec(t *testing.T) {
},
}),
pkg.MustPath("/opt", false, testtool),
), ignorePathname, wantChecksumOffline, nil},
), ignorePathname, wantOffline, nil},
})
destroyStatus(t, base, 2, 0)
testtoolDestroy(t, base, c)
}, pkg.MustDecode("PO2DSSCa4yoSgEYRcCSZfQfwow1yRigL3Ry-hI0RDI4aGuFBha-EfXeSJnG_5_Rl")},
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
{"overlay work", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/" + wantOfflineEncode: {Mode: fs.ModeDir | 0500},
"checksum/" + wantOfflineEncode + "/check": {Mode: 0400, Data: []byte{0}},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/" + expected.OvlRoot: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"substitute": {Mode: fs.ModeDir | 0700},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
{"overlay work", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{
{"container", pkg.NewExec(
"exec-overlay-work", nil, 0, false,
"exec-overlay-work", "", nil, 0, false, false,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"},
check.MustAbs("/work/bin/testtool"),
@@ -193,18 +316,35 @@ func TestExec(t *testing.T) {
return os.MkdirAll(t.GetWorkDir().String(), 0700)
},
}), pkg.Path(pkg.AbsWork, false /* ignored */, testtool),
), ignorePathname, wantChecksumOffline, nil},
), ignorePathname, wantOffline, nil},
})
destroyStatus(t, base, 2, 0)
testtoolDestroy(t, base, c)
}, pkg.MustDecode("iaRt6l_Wm2n-h5UsDewZxQkCmjZjyL8r7wv32QT2kyV55-Lx09Dq4gfg9BiwPnKs")},
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
{"multiple layers", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/" + wantOfflineEncode: {Mode: fs.ModeDir | 0500},
"checksum/" + wantOfflineEncode + "/check": {Mode: 0400, Data: []byte{0}},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/" + expected.Work: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"substitute": {Mode: fs.ModeDir | 0700},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
{"multiple layers", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{
{"container", pkg.NewExec(
"exec-multiple-layers", nil, 0, false,
"exec-multiple-layers", "", nil, 0, false, false,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"},
check.MustAbs("/opt/bin/testtool"),
@@ -245,18 +385,40 @@ func TestExec(t *testing.T) {
},
}),
pkg.MustPath("/opt", false, testtool),
), ignorePathname, wantChecksumOffline, nil},
), ignorePathname, wantOffline, nil},
})
destroyStatus(t, base, 2, 0)
testtoolDestroy(t, base, c)
}, pkg.MustDecode("O2YzyR7IUGU5J2CADy0hUZ3A5NkP_Vwzs4UadEdn2oMZZVWRtH0xZGJ3HXiimTnZ")},
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
{"overlay layer promotion", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/" + wantOfflineEncode: {Mode: fs.ModeDir | 0500},
"checksum/" + wantOfflineEncode + "/check": {Mode: 0400, Data: []byte{0}},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400, Data: []byte{}},
"checksum/nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK": {Mode: fs.ModeDir | 0500},
"checksum/nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK/check": {Mode: 0400, Data: []byte("layers")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
"identifier/B-kc5iJMx8GtlCua4dz6BiJHnDAOUfPjgpbKq4e-QEn0_CZkSYs3fOA1ve06qMs2": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK")},
"identifier/" + expected.Layers: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"substitute": {Mode: fs.ModeDir | 0700},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
{"overlay layer promotion", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{
{"container", pkg.NewExec(
"exec-layer-promotion", nil, 0, true,
"exec-layer-promotion", "", nil, 0, false, true,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"},
check.MustAbs("/opt/bin/testtool"),
@@ -276,11 +438,96 @@ func TestExec(t *testing.T) {
},
}),
pkg.MustPath("/opt", false, testtool),
), ignorePathname, wantChecksumOffline, nil},
), ignorePathname, wantOffline, nil},
})
destroyStatus(t, base, 2, 0)
testtoolDestroy(t, base, c)
}, pkg.MustDecode("3EaW6WibLi9gl03_UieiFPaFcPy5p4x3JPxrnLJxGaTI-bh3HU9DK9IMx7c3rrNm")},
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/" + wantOfflineEncode: {Mode: fs.ModeDir | 0500},
"checksum/" + wantOfflineEncode + "/check": {Mode: 0400, Data: []byte{0}},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/kvJIqZo5DKFOxC2ZQ-8_nPaQzEAz9cIm3p6guO-uLqm-xaiPu7oRkSnsu411jd_U": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"identifier/" + expected.Promote: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)},
"substitute": {Mode: fs.ModeDir | 0700},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
{"binfmt", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
if info.CanDegrade && os.Getenv("ROSA_SKIP_BINFMT") != "" {
t.Skip("binfmt_misc test explicitly skipped")
}
cureMany(t, c, []cureStep{
{"container", pkg.NewExec(
"exec-binfmt", "cafe", nil, 0, false, true,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1", "HAKUREI_BINFMT=1"},
check.MustAbs("/opt/bin/sample"),
[]string{"sample"},
pkg.MustPath("/", true, &stubArtifact{
kind: pkg.KindTar,
params: []byte("empty directory"),
cure: func(t *pkg.TContext) error {
return os.MkdirAll(t.GetWorkDir().String(), 0700)
},
}),
pkg.MustPath("/opt", false, overrideIdent{pkg.ID{0xfe, 0xff}, &stubArtifact{
kind: pkg.KindTar,
cure: func(t *pkg.TContext) error {
work := t.GetWorkDir()
if err := os.MkdirAll(
work.Append("bin").String(),
0700,
); err != nil {
return err
}
return os.WriteFile(t.GetWorkDir().Append(
"bin",
"sample",
).String(), []byte(expected.Full), 0500)
},
}}),
), ignorePathname, expectsFS{
".": {Mode: fs.ModeDir | 0500},
"check": {Mode: 0400, Data: []byte("binfmt")},
}, nil},
})
destroyStatus(t, base, 2, 0)
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/5aevg3YpDxjqQZ-pdvXK7YqgkL5JKqcoStYQxeD96kuYar6K2mRQWMHib6NQRnpV": {Mode: fs.ModeDir | 0500},
"checksum/5aevg3YpDxjqQZ-pdvXK7YqgkL5JKqcoStYQxeD96kuYar6K2mRQWMHib6NQRnpV/bin": {Mode: fs.ModeDir | 0700},
"checksum/5aevg3YpDxjqQZ-pdvXK7YqgkL5JKqcoStYQxeD96kuYar6K2mRQWMHib6NQRnpV/bin/sample": {Mode: 0500, Data: []byte("\xca\xfe\xba\xbe\xfd\xfd:3")},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"checksum/UnDo4B5KneEUY5b4vRUk_y9MWgkWuw2N8f8a2XayO686xXur-aZmX2-7n_8tKMe3": {Mode: fs.ModeDir | 0500},
"checksum/UnDo4B5KneEUY5b4vRUk_y9MWgkWuw2N8f8a2XayO686xXur-aZmX2-7n_8tKMe3/check": {Mode: 0400, Data: []byte("binfmt")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/6VQTJ1lI5BmVuI1YFYJ8ClO3MRORvTTrcWFDcUU-l5Ga8EofxCxGlSTYN-u8dKj_": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UnDo4B5KneEUY5b4vRUk_y9MWgkWuw2N8f8a2XayO686xXur-aZmX2-7n_8tKMe3")},
"identifier/_v8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/5aevg3YpDxjqQZ-pdvXK7YqgkL5JKqcoStYQxeD96kuYar6K2mRQWMHib6NQRnpV")},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"substitute": {Mode: fs.ModeDir | 0700},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
})
}
+16 -6
View File
@@ -1,6 +1,7 @@
package pkg_test
import (
"io/fs"
"testing"
"hakurei.app/check"
@@ -10,18 +11,27 @@ import (
func TestFile(t *testing.T) {
t.Parallel()
want := expectsFile{0}
checkWithCache(t, []cacheTestCase{
{"file", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
cureMany(t, c, []cureStep{
{"short", pkg.NewFile("null", []byte{0}), base.Append(
"identifier",
"3376ALA7hIUm2LbzH2fDvRezgzod1eTK_G6XjyOgbM2u-6swvkFaF0BOwSl_juBi",
), pkg.MustDecode(
"vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX",
), nil},
), want, nil},
})
}, pkg.MustDecode(
"iR6H5OIsyOW4EwEgtm9rGzGF6DVtyHLySEtwnFE8bnus9VJcoCbR4JIek7Lw-vwT",
)},
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/" + pkg.Encode(want.hash()): {Mode: 0400, Data: []byte{0}},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/3376ALA7hIUm2LbzH2fDvRezgzod1eTK_G6XjyOgbM2u-6swvkFaF0BOwSl_juBi": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX")},
"substitute": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
})
}
@@ -0,0 +1,9 @@
// Package expected contains data shared between test helper and test harness.
package expected
const (
// Magic are magic bytes in the binfmt test case.
Magic = "\xca\xfe\xba\xbe\xfd\xfd"
// Full is the full content of the binfmt test case executable.
Full = Magic + ":3"
)
@@ -0,0 +1,11 @@
package expected
const (
Offline = "q5ktDTq0miP-VvB2blxqXQeaRXCUWgP_KbC18KNtUDtyoaI_h5mHmGuPMArVEBDs"
OfflineS = "IY91PCtOpCYy21AaIK0c9f8-Z6fb2_2ewoHWkt4dxoLf0GOrWqS8yAGFLV84b1Dw"
OvlRoot = "NacZGXwuRkTvcHaG08a22ujJ8qCWN0RSoFlRSR5FSt0ZcBbJ28FRvkYsHEtX7G8i"
Layers = "WBJDrATtX6rIE5yAu8ePX3WmDF0Tt9kFiue0m3cRnyRoVx1my8a67fh3CAW486oP"
Net = "CmYtj2sNB3LHtqiDuck_Lz3MjLLIiwyP8N4NDitQ1Icvv__LVP9p8tm-sHeQaKKp"
Promote = "TX3eCloaQFkV-SZIH6Jg6E5WKH--rcXY1P0jnZKmLFKWrNqnOzd4G9eIBh6i5ywN"
Work = "OuNiLSC68pZhAOr1YQ4WbV1tzASA0nxLEBcK7lO7MqxDY_j8dmP_C612RTuF23Lu"
)
@@ -0,0 +1,11 @@
package expected
const (
Offline = "WapqyoPxbWSnq07dWHt71mHaJXq99pAjJfFlELlJljSiZMhTFqqlzU1_mN86shSj"
OfflineS = "ibQZHcdXgNQ1OiMX1FrburBbGPVvKEHvPilbQCkm_0oV0BQCHomyyTbYNrFMGIwl"
OvlRoot = "V9anFOiRvjGfAeBhLl14AL8TKdWZyD0WTPYe4fS9mOBw8iW5Lmarvt6TG6MV8uWm"
Layers = "tKx7JNRoSBdK_7MdzI-nwTNV2wmiPzwYdcd17oLmXKL_iLmUzUiA79qTqdrTasrv"
Net = "aXyDLzBCJ9XltXZIfetEVsEkrqHfcXuD5XE_FcUnYbN3emwL55N6P8LlHzNfGnM5"
Promote = "3k4V16n96Lq04gjFSKmm4sFjyQ883FFBNXgTy9s_DjeTwxT3pg_iacEh8yMb_S4m"
Work = "6Q49MhFWRE3Ne6MycwAotgl1GtoU5WCHqJNWG2byYZCY-zX-IxPrWiKk7bKkNzhE"
)
@@ -0,0 +1,11 @@
package expected
const (
Offline = "Z6yXE5gOJScL3srmnVMWgCXccDiUNZ5snSrf6RkXuU1_U0rX_kGVwsfHUgNG_awd"
OfflineS = "zN16xv6LKRJRipUJwupyxg2rZcvf-qpsMn_qCxUmgxlTSuNwYI70ZEb7dHW5k0gO"
OvlRoot = "zYXJHFRLuxvUhuisZEXgGgVvdQd6piMfp5jmtT6jdVjvC2gICXquOq-UTwlrSD5I"
Layers = "_F8EDazHbcLeT0sVSQXRN_kn9IjduqJcDYgzXpsT-hpKU4EBcZ0PISN2zchpqMbm"
Net = "CA_FAaSIYJgapBEHV40doxpH23PdUEy_6s1TZc7wfSPN0XYqwGpMceXXDSabGveO"
Promote = "_3LPrLp--4h9k4GsNNApu9hHtAafq-GUhfU6d4hJKBDKT3bz_szOsvkXxc5sK53d"
Work = "FEgHeiCD_WT4wsfB-9kDH5n6cRWCEYtJmXdKZgmUUukAOoXumH_hLlosXREC-tqq"
)
@@ -14,15 +14,29 @@ import (
"strconv"
"strings"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/vfs"
"hakurei.app/internal/pkg/internal/testtool/expected"
)
func main() {
log.SetFlags(0)
log.SetPrefix("testtool: ")
if os.Getenv("HAKUREI_BINFMT") == "1" {
wantArgs := []string{"/interpreter", "/opt/bin/sample"}
if !slices.Equal(os.Args, wantArgs) {
log.Fatalf("Args: %q, want %q", os.Args, wantArgs)
}
if err := os.WriteFile("check", []byte("binfmt"), 0400); err != nil {
log.Fatal(err)
}
return
}
environ := slices.DeleteFunc(slices.Clone(os.Environ()), func(s string) bool {
return s == "CURE_JOBS="+strconv.Itoa(runtime.NumCPU())
})
@@ -148,59 +162,40 @@ func main() {
}
const checksumEmptyDir = "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU"
ident := "dztPS6jRjiZtCF4_p8AzfnxGp6obkhrgFVsxdodbKWUoAEVtDz3MykepJB4kI_ks"
ident := expected.Offline
log.Println(m)
next := func() { m = m.Next; log.Println(m) }
if overlayRoot {
ident = "RdMA-mubnrHuu3Ky1wWyxauSYCO0ZH_zCPUj3uDHqkfwv5sGcByoF_g5PjlGiClb"
ident = expected.OvlRoot
if m.Root != "/" || m.Target != "/" ||
m.Source != "overlay" || m.FsType != "overlay" {
log.Fatal("unexpected root mount entry")
}
var lowerdir string
var lowerdir []string
for _, o := range strings.Split(m.FsOptstr, ",") {
const lowerdirKey = "lowerdir="
const lowerdirKey = "lowerdir+="
if strings.HasPrefix(o, lowerdirKey) {
lowerdir = o[len(lowerdirKey):]
lowerdir = append(lowerdir, o[len(lowerdirKey):])
}
}
if !layers {
if filepath.Base(lowerdir) != checksumEmptyDir {
if len(lowerdir) != 1 || filepath.Base(lowerdir[0]) != checksumEmptyDir {
log.Fatal("unexpected artifact checksum")
}
} else {
ident = "p1t_drXr34i-jZNuxDMLaMOdL6tZvQqhavNafGynGqxOZoXAUTSn7kqNh3Ovv3DT"
ident = expected.Layers
lowerdirsEscaped := strings.Split(lowerdir, ":")
lowerdirs := lowerdirsEscaped[:0]
// ignore the option separator since it does not appear in ident
for i, e := range lowerdirsEscaped {
if len(e) > 0 &&
e[len(e)-1] == check.SpecialOverlayEscape[0] &&
(len(e) == 1 || e[len(e)-2] != check.SpecialOverlayEscape[0]) {
// ignore escaped pathname separator since it does not
// appear in ident
e = e[:len(e)-1]
if len(lowerdirsEscaped) != i {
lowerdirsEscaped[i+1] = e + lowerdirsEscaped[i+1]
continue
}
}
lowerdirs = append(lowerdirs, e)
}
if len(lowerdirs) != 2 ||
filepath.Base(lowerdirs[0]) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" ||
filepath.Base(lowerdirs[1]) != "nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK" {
log.Fatalf("unexpected lowerdirs %s", strings.Join(lowerdirs, ", "))
if len(lowerdir) != 2 ||
filepath.Base(lowerdir[0]) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" ||
filepath.Base(lowerdir[1]) != "nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK" {
log.Fatalf("unexpected lowerdirs %s", strings.Join(lowerdir, ", "))
}
}
} else {
if hostNet {
ident = "G8qPxD9puvvoOVV7lrT80eyDeIl3G_CCFoKw12c8mCjMdG1zF7NEPkwYpNubClK3"
ident = expected.Net
}
if m.Root != "/sysroot" || m.Target != "/" {
@@ -219,14 +214,14 @@ func main() {
}
if promote {
ident = "xXTIYcXmgJWNLC91c417RRrNM9cjELwEZHpGvf8Fk_GNP5agRJp_SicD0w9aMeLJ"
ident = expected.Promote
}
next() // testtool artifact
next()
if overlayWork {
ident = "5hlaukCirnXE4W_RSLJFOZN47Z5RiHnacXzdFp_70cLgiJUGR6cSb_HaFftkzi0-"
ident = expected.Work
if m.Root != "/" || m.Target != "/work" ||
m.Source != "overlay" || m.FsType != "overlay" {
log.Fatal("unexpected work mount entry")
+67 -12
View File
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io"
"iter"
"slices"
"strconv"
"sync"
@@ -20,7 +21,7 @@ import (
const wordSize = 8
// alignSize returns the padded size for aligning sz.
func alignSize(sz int) int {
func alignSize[T int | uint64](sz T) T {
return sz + (wordSize-(sz)%wordSize)%wordSize
}
@@ -65,6 +66,18 @@ func NewIR() *IRCache {
return &IRCache{zeroIRCache()}
}
// Inputs returns an iterator over direct and transitive inputs of an [Artifact]
// in randomised order.
func Inputs(a Artifact) iter.Seq2[Artifact, unique.Handle[ID]] {
ic := NewIR()
ic.Ident(a)
return func(yield func(Artifact, unique.Handle[ID]) bool) {
ic.artifact.Range(func(key, value any) bool {
return yield(key.(Artifact), value.(unique.Handle[ID]))
})
}
}
// IContext is passed to [Artifact.Params] and provides methods for writing
// values to the IR writer. It does not expose the underlying [io.Writer].
//
@@ -76,6 +89,9 @@ type IContext struct {
// Written to by various methods, should be zeroed after [Artifact.Params]
// returns and must not be exposed directly.
w io.Writer
// Optional [Artifact] to cureRes cache, replaces [IRKindIdent] with
// checksum values if non-nil.
inputs map[Artifact]cureRes
}
// irZero is a zero IR word.
@@ -163,7 +179,15 @@ func (i *IContext) WriteIdent(a Artifact) {
defer i.ic.putIdentBuf(buf)
IRKindIdent.encodeHeader(0).put(buf[:])
*(*ID)(buf[wordSize:]) = i.ic.Ident(a).Value()
if i.inputs != nil {
res, ok := i.inputs[a]
if !ok {
panic(InvalidLookupError(i.ic.Ident(a).Value()))
}
*(*ID)(buf[wordSize:]) = res.checksum.Value()
} else {
*(*ID)(buf[wordSize:]) = i.ic.Ident(a).Value()
}
i.mustWrite(buf[:])
}
@@ -207,19 +231,44 @@ func (i *IContext) WriteString(s string) {
// Encode writes a deterministic, efficient representation of a to w and returns
// the first non-nil error encountered while writing to w.
func (ic *irCache) Encode(w io.Writer, a Artifact) (err error) {
return ic.encode(w, a, nil)
}
// encode implements Encode but replaces identifiers with their cured checksums
// for a non-nil ident. Caller must acquire Cache.identMu.
func (ic *irCache) encode(
w io.Writer,
a Artifact,
inputs map[Artifact]cureRes,
) (err error) {
deps := a.Dependencies()
idents := make([]*extIdent, len(deps))
for i, d := range deps {
dbuf, did := ic.unsafeIdent(d, true)
if dbuf == nil {
dbuf = ic.getIdentBuf()
if inputs == nil {
for i, d := range deps {
dbuf, did := ic.unsafeIdent(d, true)
if dbuf == nil {
dbuf = ic.getIdentBuf()
binary.LittleEndian.PutUint64(dbuf[:], uint64(d.Kind()))
*(*ID)(dbuf[wordSize:]) = did.Value()
} else {
ic.storeIdent(d, dbuf)
}
defer ic.putIdentBuf(dbuf)
idents[i] = dbuf
}
} else {
for i, d := range deps {
res, ok := inputs[d]
if !ok {
return InvalidLookupError(ic.Ident(d).Value())
}
dbuf := ic.getIdentBuf()
binary.LittleEndian.PutUint64(dbuf[:], uint64(d.Kind()))
*(*ID)(dbuf[wordSize:]) = did.Value()
} else {
ic.storeIdent(d, dbuf)
*(*ID)(dbuf[wordSize:]) = res.checksum.Value()
defer ic.putIdentBuf(dbuf)
idents[i] = dbuf
}
defer ic.putIdentBuf(dbuf)
idents[i] = dbuf
}
slices.SortFunc(idents, func(a, b *extIdent) int {
return bytes.Compare(a[:], b[:])
@@ -244,7 +293,7 @@ func (ic *irCache) Encode(w io.Writer, a Artifact) (err error) {
}
func() {
i := IContext{ic, w}
i := IContext{ic, w, inputs}
defer panicToError(&err)
defer func() { i.ic, i.w = nil, nil }()
@@ -432,6 +481,12 @@ func (e InvalidKindError) Error() string {
// register is not safe for concurrent use. register must not be called after
// the first instance of [Cache] has been opened.
func register(k Kind, f IRReadFunc) {
openMu.Lock()
defer openMu.Unlock()
if opened {
panic("attempting to register after open")
}
if _, ok := irArtifact[k]; ok {
panic("attempting to register " + strconv.Itoa(int(k)) + " twice")
}
+45 -22
View File
@@ -3,6 +3,7 @@ package pkg_test
import (
"bytes"
"io"
"io/fs"
"reflect"
"testing"
@@ -26,19 +27,17 @@ func TestIRRoundtrip(t *testing.T) {
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
)},
{"http get tar", pkg.NewHTTPGetTar(
{"http get tar", pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet(
nil, "file:///testdata",
pkg.Checksum(bytes.Repeat([]byte{0xff}, len(pkg.Checksum{}))),
pkg.TarBzip2,
)},
{"http get tar unaligned", pkg.NewHTTPGetTar(
), pkg.Bzip2))},
{"http get tar unaligned", pkg.NewTar(pkg.NewHTTPGet(
nil, "https://hakurei.app",
pkg.Checksum(bytes.Repeat([]byte{0xfe}, len(pkg.Checksum{}))),
pkg.TarUncompressed,
)},
))},
{"exec offline", pkg.NewExec(
"exec-offline", nil, 0, false,
"exec-offline", "", nil, 0, false, false,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1"},
check.MustAbs("/opt/bin/testtool"),
@@ -46,21 +45,19 @@ func TestIRRoundtrip(t *testing.T) {
pkg.MustPath("/file", false, pkg.NewFile("file", []byte(
"stub file",
))), pkg.MustPath("/.hakurei", false, pkg.NewHTTPGetTar(
))), pkg.MustPath("/.hakurei", false, pkg.NewTar(pkg.NewHTTPGet(
nil, "file:///hakurei.tar",
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
pkg.TarUncompressed,
)), pkg.MustPath("/opt", false, pkg.NewHTTPGetTar(
))), pkg.MustPath("/opt", false, pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet(
nil, "file:///testtool.tar.gz",
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
pkg.TarGzip,
)),
), pkg.Gzip))),
)},
{"exec net", pkg.NewExec(
"exec-net",
"exec-net", "",
(*pkg.Checksum)(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
0, false,
0, false, false,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1"},
check.MustAbs("/opt/bin/testtool"),
@@ -68,19 +65,41 @@ func TestIRRoundtrip(t *testing.T) {
pkg.MustPath("/file", false, pkg.NewFile("file", []byte(
"stub file",
))), pkg.MustPath("/.hakurei", false, pkg.NewHTTPGetTar(
))), pkg.MustPath("/.hakurei", false, pkg.NewTar(pkg.NewHTTPGet(
nil, "file:///hakurei.tar",
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
pkg.TarUncompressed,
)), pkg.MustPath("/opt", false, pkg.NewHTTPGetTar(
))), pkg.MustPath("/opt", false, pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet(
nil, "file:///testtool.tar.gz",
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
pkg.TarGzip,
)),
), pkg.Gzip))),
)},
{"exec measured", pkg.NewExec(
"exec-measured", "",
(*pkg.Checksum)(bytes.Repeat([]byte{0xfd}, len(pkg.Checksum{}))),
0, false, false,
pkg.AbsWork,
[]string{"HAKUREI_TEST=1"},
check.MustAbs("/opt/bin/testtool"),
[]string{"testtool", "measured"},
pkg.MustPath("/file", false, pkg.NewFile("file", []byte(
"stub file",
))), pkg.MustPath("/.hakurei", false, pkg.NewTar(pkg.NewHTTPGet(
nil, "file:///hakurei.tar",
pkg.Checksum(bytes.Repeat([]byte{0xfd}, len(pkg.Checksum{}))),
))), pkg.MustPath("/opt", false, pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet(
nil, "file:///testtool.tar.gz",
pkg.Checksum(bytes.Repeat([]byte{0xfd}, len(pkg.Checksum{}))),
), pkg.Gzip))),
)},
{"file anonymous", pkg.NewFile("", []byte{0})},
{"file", pkg.NewFile("stub", []byte("stub"))},
{"decompress", pkg.NewDecompress(pkg.NewFile("", []byte{0}), pkg.Bzip2)},
{"archive", pkg.NewArchive(pkg.NewFile("", []byte{0}))},
}
testCasesCache := make([]cacheTestCase, len(testCases))
for i, tc := range testCases {
@@ -105,9 +124,13 @@ func TestIRRoundtrip(t *testing.T) {
if err := <-done; err != nil {
t.Fatalf("EncodeAll: error = %v", err)
}
}, pkg.MustDecode(
"E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C",
),
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"identifier": {Mode: fs.ModeDir | 0700},
"substitute": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
},
}
}
checkWithCache(t, testCasesCache)
+21 -2
View File
@@ -3,6 +3,7 @@ package pkg_test
import (
"crypto/sha512"
"io"
"io/fs"
"net/http"
"reflect"
"testing"
@@ -85,7 +86,13 @@ func TestHTTPGet(t *testing.T) {
if _, err := f.Cure(r); !reflect.DeepEqual(err, wantErrNotFound) {
t.Fatalf("Cure: error = %#v, want %#v", err, wantErrNotFound)
}
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C")},
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"identifier": {Mode: fs.ModeDir | 0700},
"substitute": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
{"cure", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
r := newRContext(t, c)
@@ -144,6 +151,18 @@ func TestHTTPGet(t *testing.T) {
if _, _, err := c.Cure(f); !reflect.DeepEqual(err, wantErrNotFound) {
t.Fatalf("Pathname: error = %#v, want %#v", err, wantErrNotFound)
}
}, pkg.MustDecode("L_0RFHpr9JUS4Zp14rz2dESSRvfLzpvqsLhR1-YjQt8hYlmEdVl7vI3_-v8UNPKs")},
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/fLYGIMHgN1louE-JzITJZJo2SDniPu-IHBXubtvQWFO-hXnDVKNuscV7-zlyr5fU": {Mode: 0400, Data: []byte("\x7f\xe1\x69\xa2\xdd\x63\x96\x26\x83\x79\x61\x8b\xf0\x3f\xd5\x16\x9a\x39\x3a\xdb\xcf\xb1\xbc\x8d\x33\xff\x75\xee\x62\x56\xa9\xf0\x27\xac\x13\x94\x69")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/oM-2pUlk-mOxK1t3aMWZer69UdOQlAXiAgMrpZ1476VoOqpYVP1aGFS9_HYy-D8_": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fLYGIMHgN1louE-JzITJZJo2SDniPu-IHBXubtvQWFO-hXnDVKNuscV7-zlyr5fU")},
"substitute": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
})
}
+734 -109
View File
File diff suppressed because it is too large Load Diff
+1037 -161
View File
File diff suppressed because it is too large Load Diff
+17 -66
View File
@@ -2,32 +2,18 @@ package pkg
import (
"archive/tar"
"compress/bzip2"
"compress/gzip"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
)
const (
// TarUncompressed denotes an uncompressed tarball.
TarUncompressed = iota
// TarGzip denotes a tarball compressed via [gzip].
TarGzip
// TarBzip2 denotes a tarball compressed via [bzip2].
TarBzip2
)
// A tarArtifact is an [Artifact] unpacking a tarball backed by a [FileArtifact].
type tarArtifact struct {
// Caller-supplied backing tarball.
f Artifact
// Compression on top of the tarball.
compression uint32
}
// tarArtifactNamed embeds tarArtifact for a [fmt.Stringer] tarball.
@@ -39,13 +25,13 @@ type tarArtifactNamed struct {
var _ fmt.Stringer = new(tarArtifactNamed)
// String returns the name of the underlying [Artifact] suffixed with unpack.
func (a *tarArtifactNamed) String() string { return a.name + "-unpack" }
// String returns the name of the underlying [Artifact] prefixed with unpack.
func (a *tarArtifactNamed) String() string { return "unpack-" + a.name }
// NewTar returns a new [Artifact] backed by the supplied [Artifact] and
// compression method. The source [Artifact] must be a [FileArtifact].
func NewTar(a Artifact, compression uint32) Artifact {
ta := tarArtifact{a, compression}
// NewTar returns a new [Artifact] unpacking the tar stream produced by the
// backing [Artifact]. The source [Artifact] must be a [FileArtifact].
func NewTar(a Artifact) Artifact {
ta := tarArtifact{a}
if s, ok := a.(fmt.Stringer); ok {
if name := s.String(); name != "" {
return &tarArtifactNamed{ta, name}
@@ -54,25 +40,15 @@ func NewTar(a Artifact, compression uint32) Artifact {
return &ta
}
// NewHTTPGetTar is abbreviation for NewHTTPGet passed to NewTar.
func NewHTTPGetTar(
hc *http.Client,
url string,
checksum Checksum,
compression uint32,
) Artifact {
return NewTar(NewHTTPGet(hc, url, checksum), compression)
}
// Kind returns the hardcoded [Kind] constant.
func (a *tarArtifact) Kind() Kind { return KindTar }
// Params writes compression encoded in little endian.
func (a *tarArtifact) Params(ctx *IContext) { ctx.WriteUint32(a.compression) }
// Params is a noop.
func (a *tarArtifact) Params(*IContext) {}
func init() {
register(KindTar, func(r *IRReader) Artifact {
a := NewTar(r.Next(), r.ReadUint32())
a := NewTar(r.Next())
if _, ok := r.Finalise(); ok {
panic(ErrUnexpectedChecksum)
}
@@ -98,42 +74,17 @@ func (e DisallowedTypeflagError) Error() string {
// Cure cures the [Artifact], producing a directory located at work.
func (a *tarArtifact) Cure(t *TContext) (err error) {
var tr io.ReadCloser
if tr, err = t.Open(a.f); err != nil {
var r io.ReadCloser
if r, err = t.Open(a.f); err != nil {
return
}
defer func(f io.ReadCloser) {
if err == nil {
err = tr.Close()
}
closeErr := f.Close()
defer func() {
closeErr := r.Close()
if err == nil {
err = closeErr
}
}(tr)
br := t.cache.getReader(tr)
defer t.cache.putReader(br)
tr = io.NopCloser(br)
switch a.compression {
case TarUncompressed:
break
case TarGzip:
if tr, err = gzip.NewReader(tr); err != nil {
return
}
break
case TarBzip2:
tr = io.NopCloser(bzip2.NewReader(tr))
break
default:
return os.ErrInvalid
}
}()
type dirTargetPerm struct {
path string
@@ -156,8 +107,8 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
}()
var header *tar.Header
r := tar.NewReader(tr)
for header, err = r.Next(); err == nil; header, err = r.Next() {
tr := tar.NewReader(r)
for header, err = tr.Next(); err == nil; header, err = tr.Next() {
typeflag := header.Typeflag
if typeflag == 0 {
if len(header.Name) > 0 && header.Name[len(header.Name)-1] == '/' {
@@ -183,7 +134,7 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
); err != nil {
return
}
if _, err = io.Copy(f, r); err != nil {
if _, err = io.Copy(f, tr); err != nil {
_ = f.Close()
return
} else if err = f.Close(); err != nil {
+84 -66
View File
@@ -20,6 +20,31 @@ import (
func TestTar(t *testing.T) {
t.Parallel()
want := expectsFS{
".": {Mode: fs.ModeDir | 0500},
"checksum": {Mode: fs.ModeDir | 0500},
"checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP": {Mode: fs.ModeDir | 0500},
"checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check": {Mode: 0400, Data: []byte{0, 0}},
"checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib": {Mode: fs.ModeDir | 0500},
"checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig": {Mode: fs.ModeDir | 0500},
"checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
"identifier": {Mode: fs.ModeDir | 0500},
"identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
"identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
"work": {Mode: fs.ModeDir | 0500},
}
wantEncode := pkg.Encode(want.hash())
wantExpand := expectsFS{
".": {Mode: fs.ModeDir | 0500},
"libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
}
wantExpandEncode := pkg.Encode(wantExpand.hash())
checkWithCache(t, []cacheTestCase{
{"http", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
checkTarHTTP(t, base, c, fstest.MapFS{
@@ -37,10 +62,32 @@ func TestTar(t *testing.T) {
"identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
"work": {Mode: fs.ModeDir | 0700},
}, pkg.MustDecode(
"cTw0h3AmYe7XudSoyEMByduYXqGi-N5ZkTZ0t9K5elsu3i_jNIVF5T08KR1roBFM",
))
}, pkg.MustDecode("NQTlc466JmSVLIyWklm_u8_g95jEEb98PxJU-kjwxLpfdjwMWJq0G8ze9R4Vo1Vu")},
}, want)
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/" + wantEncode: {Mode: fs.ModeDir | 0500},
"checksum/" + wantEncode + "/checksum": {Mode: fs.ModeDir | 0500},
"checksum/" + wantEncode + "/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP": {Mode: fs.ModeDir | 0500},
"checksum/" + wantEncode + "/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check": {Mode: 0400, Data: []byte{0, 0}},
"checksum/" + wantEncode + "/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib": {Mode: fs.ModeDir | 0500},
"checksum/" + wantEncode + "/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
"checksum/" + wantEncode + "/checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig": {Mode: fs.ModeDir | 0500},
"checksum/" + wantEncode + "/identifier": {Mode: fs.ModeDir | 0500},
"checksum/" + wantEncode + "/identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
"checksum/" + wantEncode + "/identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP")},
"checksum/" + wantEncode + "/work": {Mode: fs.ModeDir | 0500},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/T68YqeEW5moiwVe4J0wPvjERgyMUd4k_cKKxTcQx6AXqZaQAuuryu-Iv-qxDV6-T": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)},
"identifier/WbWIIUGl6bqDBsjU-UB69JqOWLjG5waiUJBUBGeeRp3V7FBbwaBe3sG1qm7DlPSk": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)},
"substitute": {Mode: fs.ModeDir | 0700},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
{"http expand", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
checkTarHTTP(t, base, c, fstest.MapFS{
@@ -48,10 +95,23 @@ func TestTar(t *testing.T) {
"lib": {Mode: fs.ModeDir | 0700},
"lib/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
}, pkg.MustDecode(
"CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN",
))
}, pkg.MustDecode("hSoSSgCYTNonX3Q8FjvjD1fBl-E-BQyA6OTXro2OadXqbST4tZ-akGXszdeqphRe")},
}, wantExpand)
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/" + wantExpandEncode: {Mode: fs.ModeDir | 0500},
"checksum/" + wantExpandEncode + "/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/WbWIIUGl6bqDBsjU-UB69JqOWLjG5waiUJBUBGeeRp3V7FBbwaBe3sG1qm7DlPSk": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)},
"identifier/e3BECw_x_vDyTvj2P48MJVgwpgALeXQ90Ye2Y8gr6SwIPtMup6SR2LVcCTDRGpVi": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)},
"substitute": {Mode: fs.ModeDir | 0700},
"temp": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
})
}
@@ -60,7 +120,7 @@ func checkTarHTTP(
base *check.Absolute,
c *pkg.Cache,
testdataFsys fs.FS,
wantChecksum pkg.Checksum,
want expectsKnown,
) {
var testdata string
{
@@ -96,49 +156,6 @@ func checkTarHTTP(
"testdata": {Data: []byte(testdata), Mode: 0400},
}))
wantIdent := func() pkg.ID {
h := sha512.New384()
// kind uint64
h.Write([]byte{byte(pkg.KindTar), 0, 0, 0, 0, 0, 0, 0})
// deps_sz uint64
h.Write([]byte{1, 0, 0, 0, 0, 0, 0, 0})
// kind uint64
h.Write([]byte{byte(pkg.KindHTTPGet), 0, 0, 0, 0, 0, 0, 0})
// ident ID
h0 := sha512.New384()
// kind uint64
h0.Write([]byte{byte(pkg.KindHTTPGet), 0, 0, 0, 0, 0, 0, 0})
// deps_sz uint64
h0.Write([]byte{0, 0, 0, 0, 0, 0, 0, 0})
// url string
h0.Write([]byte{byte(pkg.IRKindString), 0, 0, 0})
h0.Write([]byte{0x10, 0, 0, 0})
h0.Write([]byte("file:///testdata"))
// end(KnownChecksum)
h0.Write([]byte{byte(pkg.IRKindEnd), 0, 0, 0})
h0.Write([]byte{byte(pkg.IREndKnownChecksum), 0, 0, 0})
// checksum Checksum
h0.Write(testdataChecksum[:])
h.Write(h0.Sum(nil))
// compression uint32
h.Write([]byte{byte(pkg.IRKindUint32), 0, 0, 0})
h.Write([]byte{pkg.TarGzip, 0, 0, 0})
// end
h.Write([]byte{byte(pkg.IRKindEnd), 0, 0, 0})
h.Write([]byte{0, 0, 0, 0})
return pkg.ID(h.Sum(nil))
}()
a := pkg.NewHTTPGetTar(
&client,
"file:///testdata",
testdataChecksum,
pkg.TarGzip,
)
tarDir := stubArtifact{
kind: pkg.KindExec,
params: []byte("directory containing a single regular file"),
@@ -191,27 +208,28 @@ func checkTarHTTP(
defer newDestroyArtifactFunc(&tarDirType)(t, base, c)
cureMany(t, c, []cureStep{
{"file", a, base.Append(
"identifier",
pkg.Encode(wantIdent),
), wantChecksum, nil},
{"file", pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet(
&client,
"file:///testdata",
testdataChecksum,
), pkg.Gzip)), ignorePathname, want, nil},
{"directory", pkg.NewTar(
{"directory", pkg.NewTar(pkg.NewDecompress(
&tarDir,
pkg.TarGzip,
), ignorePathname, wantChecksum, nil},
pkg.Gzip,
)), ignorePathname, want, nil},
{"multiple entries", pkg.NewTar(
{"multiple entries", pkg.NewTar(pkg.NewDecompress(
&tarDirMulti,
pkg.TarGzip,
), nil, pkg.Checksum{}, errors.New(
pkg.Gzip,
)), nil, nil, errors.New(
"input directory does not contain a single regular file",
)},
{"bad type", pkg.NewTar(
{"bad type", pkg.NewTar(pkg.NewDecompress(
&tarDirType,
pkg.TarGzip,
), nil, pkg.Checksum{}, errors.New(
pkg.Gzip,
)), nil, nil, errors.New(
"input directory does not contain a single regular file",
)},
@@ -221,6 +239,6 @@ func checkTarHTTP(
cure: func(t *pkg.TContext) error {
return stub.UniqueError(0xcafe)
},
}, pkg.TarGzip), nil, pkg.Checksum{}, stub.UniqueError(0xcafe)},
}), nil, nil, stub.UniqueError(0xcafe)},
})
}
+189
View File
@@ -0,0 +1,189 @@
package report
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"strconv"
"sync"
"sync/atomic"
"time"
"hakurei.app/check"
)
const (
// Trivial denotes an error that may happen frequently under normal operation.
Trivial = iota
// Inconsistent denotes an error diagnosed due to inconsistent state.
Inconsistent
// Degraded denotes an error condition causing a degraded state.
Degraded
// Fatal denotes an unrecoverable error. This is generally followed by
// notifying the user and suggesting immediate restart, or an automatic
// restart of a nonessential daemon process.
Fatal
)
// A Reporter represents an error reporting object backed by user-facing display
// and optional persistent storage. A Reporter can be used simultaneously from
// multiple goroutines.
type Reporter struct {
// Backing logger, the zero value implies the return value of [log.Default].
log atomic.Pointer[log.Logger]
// Backing persistent storage.
pathname atomic.Pointer[check.Absolute]
// Reporting behaviour.
flag atomic.Uint64
// Caller-supplied reporting functions.
notify []func(e Error)
// Synchronises access to notify.
notifyMu sync.RWMutex
// Synchronises access to persistent storage.
mu sync.Mutex
}
// SetOutput sets the backing [log.Logger].
func (r *Reporter) SetOutput(l *log.Logger) { r.log.Store(l) }
// SetPathname sets the pathname to persistent storage.
func (r *Reporter) SetPathname(a *check.Absolute) { r.pathname.Store(a) }
const (
// DStrict causes all dispatches to end in a call to panic.
DStrict = 1 << iota
// DNoRecover disallows recovering from error write to persistent storage.
DNoRecover
// DBypassEarly allows persistent storage to be skipped before it is ready.
DBypassEarly
)
// SetFlags sets flags for r.
func (r *Reporter) SetFlags(flag uint64) { r.flag.Store(flag) }
// An Error represents an error reported via [Reporter.Dispatch].
type Error struct {
// Nature of error.
Severity int
// User-facing reporting message.
Message string
// Underlying error.
Err error
}
// RepresentableError is implemented by errors friendly to JSON serialisation.
type RepresentableError interface {
error
Representable()
}
// MarshalJSON returns an incomplete JSON representation of e.
func (e Error) MarshalJSON() (data []byte, err error) {
v := struct {
Severity any `json:"kind"`
Message string `json:"message"`
Err json.RawMessage `json:"error"`
}{Message: e.Message}
switch e.Severity {
case Trivial:
v.Severity = "trivial"
case Inconsistent:
v.Severity = "inconsistent"
case Degraded:
v.Severity = "degradation"
case Fatal:
v.Severity = "fatal"
default:
v.Severity = e.Severity
}
if re, ok := errors.AsType[RepresentableError](e.Err); ok {
v.Err, err = json.Marshal(re)
} else {
v.Err, err = json.Marshal(e.Err.Error())
}
if err != nil {
return
}
return json.Marshal(&v)
}
// Error is generally only called during a [Reporter.Dispatch] call ending in
// a panic or an otherwise unrecoverable condition.
func (e Error) Error() string {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
// Notify arranges for f to be called for all future dispatched errors.
func (r *Reporter) Notify(f func(Error)) {
r.notifyMu.Lock()
r.notify = append(r.notify, f)
r.notifyMu.Unlock()
}
// getLog returns the address of the underlying [log.Logger], or the return
// value of [log.Default].
func (r *Reporter) getLog() *log.Logger {
l := r.log.Load()
if l == nil {
l = log.Default()
}
return l
}
// dispatch implements Dispatch but returns the reporting error.
func (r *Reporter) dispatch(severity int, message string, e error) (err error) {
p := Error{severity, message, e}
r.getLog().Println(message+":", e)
flag := r.flag.Load()
defer func() {
if flag&DNoRecover != 0 && err != nil {
panic(err)
}
}()
if a := r.pathname.Load(); a != nil {
var w *os.File
r.mu.Lock()
w, err = os.OpenFile(
a.Append(strconv.FormatUint(uint64(time.Now().UnixNano()), 10)).String(),
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
0400,
)
if err != nil {
r.mu.Unlock()
return
}
err = json.NewEncoder(w).Encode(p)
if _err := w.Close(); err == nil {
err = _err
}
r.mu.Unlock()
} else if flag&DBypassEarly == 0 && severity != Trivial {
panic(p)
}
if flag&DStrict != 0 && severity != Trivial {
panic(p)
}
r.notifyMu.RLock()
defer r.notifyMu.RUnlock()
for _, f := range r.notify {
f(p)
}
return
}
// Dispatch reports an error and saves it to backing storage if available.
func (r *Reporter) Dispatch(severity int, message string, e error) {
if err := r.dispatch(severity, message, e); err != nil {
r.getLog().Println(err)
}
}
+214
View File
@@ -0,0 +1,214 @@
package report_test
import (
"bytes"
"errors"
"log"
"os"
"reflect"
"slices"
"testing"
"unsafe"
"hakurei.app/check"
"hakurei.app/internal/kobject"
"hakurei.app/internal/report"
"hakurei.app/internal/stub"
"hakurei.app/internal/uevent"
)
type representableUE struct{ stub.UniqueError }
func (representableUE) Representable() {}
func TestDispatch(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
flag uint64
errs []report.Error
persist bool
wantFiles []string
wantLog string
wantPanic error
}{
{"default", 0, []report.Error{
{
Severity: report.Fatal,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
},
{
Severity: report.Inconsistent,
Message: "processed inconsistent uevent",
Err: (&kobject.Event{
Action: uevent.KOBJ_ADD,
DevPath: "\x00",
Env: map[string]string{"V": "\xfd"},
Sequence: 2,
}).NewError(kobject.EDuplicateAdd, &kobject.Object{
State: kobject.StateNew,
DevPath: "\x00",
Env: map[string]string{"V": "\xff"},
}),
},
}, true, []string{`{"kind":"fatal","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"}
`, `{"kind":"inconsistent","message":"processed inconsistent uevent","error":{"fault":1,"event":{"action":"add","devpath":"\u0000","env":{"V":"\ufffd"},"seqnum":2,"subsystem":""},"object":{"state":1,"devpath":"\u0000","subsystem":"","env":{"V":"\ufffd"}}}}
`}, `dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite
dispatch: processed inconsistent uevent: duplicate add event on devpath "\x00"
`, nil},
{"strict", report.DStrict, []report.Error{
{
Severity: report.Degraded,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
},
}, true, []string{
`{"kind":"degradation","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"}
`,
}, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", report.Error{
Severity: report.Degraded,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
}},
{"strict no recover", report.DStrict | report.DNoRecover, []report.Error{
{
Severity: report.Inconsistent,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
},
}, true, []string{
`{"kind":"inconsistent","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"}
`,
}, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", report.Error{
Severity: report.Inconsistent,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
}},
{"no recover", report.DNoRecover, []report.Error{
{
Severity: 0xbeef,
Message: "rejecting coldboot loop",
Err: representableUE{stub.UniqueError(0xcafe)},
},
}, true, []string{
`{"kind":48879,"message":"rejecting coldboot loop","error":{"UniqueError":51966}}
`,
}, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", nil},
{"early", 0, []report.Error{
{
Severity: report.Fatal,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
},
}, false, nil, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", report.Error{
Severity: report.Fatal,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
}},
{"bypass early", report.DBypassEarly, []report.Error{
{
Severity: report.Fatal,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
},
}, false, nil, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var r report.Reporter
r.SetFlags(tc.flag)
var buf bytes.Buffer
l := log.New(&buf, "dispatch: ", 0)
r.SetOutput(l)
a := check.MustAbs(t.TempDir())
if tc.persist {
r.SetPathname(a)
}
var got []report.Error
r.Notify(func(p report.Error) { got = append(got, p) })
var gotPanic any
func() {
defer func() { gotPanic = recover() }()
for _, p := range tc.errs {
r.Dispatch(p.Severity, p.Message, p.Err)
}
}()
if gotPanic == nil && !reflect.DeepEqual(got, tc.errs) {
t.Errorf("Dispatch: %#v, want %#v", got, tc.errs)
}
if !reflect.DeepEqual(gotPanic, tc.wantPanic) {
t.Errorf("Dispatch: panic = %v, want %v", gotPanic, tc.wantPanic)
}
if gotLog := buf.String(); gotLog != tc.wantLog {
t.Errorf("Dispatch: log =\n%s\nwant\n%s", gotLog, tc.wantLog)
}
if tc.persist {
var names []string
if dents, err := os.ReadDir(a.String()); err != nil {
t.Fatal(err)
} else {
names = make([]string, len(dents))
for i, dent := range dents {
names[i] = dent.Name()
}
slices.Sort(names)
}
gotFiles := make([]string, len(names))
for i, name := range names {
if p, err := os.ReadFile(a.Append(name).String()); err != nil {
t.Fatal(err)
} else {
gotFiles[i] = unsafe.String(unsafe.SliceData(p), len(p))
}
}
if !slices.Equal(gotFiles, tc.wantFiles) {
t.Errorf("Dispatch: persist = %s, want %s", gotFiles, tc.wantFiles)
}
}
})
}
}
func TestBadPersist(t *testing.T) {
t.Parallel()
var r report.Reporter
r.SetFlags(report.DNoRecover)
r.SetPathname(check.MustAbs("/proc/nonexistent"))
var pathError *os.PathError
func() {
defer func() {
if !errors.As(recover().(error), &pathError) {
t.Fatal("invalid panic kind")
}
}()
r.Dispatch(report.Fatal, "\x00", stub.UniqueError(0xbad))
}()
if !errors.Is(pathError.Err, os.ErrNotExist) {
t.Fatalf("Dispatch: panic = %v", pathError)
}
var buf bytes.Buffer
l := log.New(&buf, "persist: ", 0)
r.SetOutput(l)
r.SetFlags(0)
r.Dispatch(report.Fatal, "\x00", stub.UniqueError(0xbad))
}

Some files were not shown because too many files have changed in this diff Show More