Compare commits
264 Commits
130e470b60
...
v0.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
0cf0e18e35
|
|||
|
ee5c0dd135
|
|||
|
92c48d82e2
|
|||
|
c79a4fe7f8
|
|||
|
0aeb2bccfb
|
|||
|
50e079b99f
|
|||
|
fb2cb5005a
|
|||
|
6e73c28a92
|
|||
|
2c08aa3674
|
|||
|
1af73ae7b4
|
|||
|
c9aa5e04b1
|
|||
|
70a38bd3b0
|
|||
|
533b15da89
|
|||
|
a890e1d0e5
|
|||
|
e3520835bb
|
|||
|
0e56847754
|
|||
|
145d03b366
|
|||
|
2886228d40
|
|||
|
e1e499b79e
|
|||
|
65b7dd8b37
|
|||
|
8d72b9e5bd
|
|||
|
8a3c3d145a
|
|||
|
575ef307ad
|
|||
|
d4144fcf7f
|
|||
|
bad66facbc
|
|||
|
4aba014eac
|
|||
|
779ba994ce
|
|||
|
917be2de93
|
|||
|
9aad98d409
|
|||
|
b0d06b67dc
|
|||
|
089100f29d
|
|||
|
dfd26abf6c
|
|||
|
617ee21647
|
|||
|
15cdb37ec2
|
|||
|
1f0bdc7aca
|
|||
|
e3ffe85670
|
|||
|
f4403ba5cd
|
|||
|
5a26895a22
|
|||
|
09d9f766a9
|
|||
|
6558169666
|
|||
|
cccf970c57
|
|||
|
57ffb21690
|
|||
|
9c560b455a
|
|||
|
4c7c0fbfc6
|
|||
|
18b3b7904e
|
|||
|
fefefdf734
|
|||
|
b84bb09a80
|
|||
|
337bf20f50
|
|||
|
1cb792cf6e
|
|||
|
b2b40b07e8
|
|||
|
da11b26ec1
|
|||
|
024489e800
|
|||
|
0f795712b0
|
|||
|
7e2210ff71
|
|||
|
a71a008f3c
|
|||
|
162265b47e
|
|||
|
3fa7ac04e4
|
|||
|
bf2867d653
|
|||
|
ec0f0f6507
|
|||
|
a77a802955
|
|||
|
4407e14dfc
|
|||
|
e024d3184a
|
|||
|
8e1bf00c2d
|
|||
|
b111e22050
|
|||
|
1fa458c0be
|
|||
|
2c7ae67a67
|
|||
|
3826621b21
|
|||
|
041b505c2e
|
|||
|
e6debce649
|
|||
|
aa26b86fce
|
|||
|
a57a8fd5d8
|
|||
|
1d5d063d6a
|
|||
|
e61628a34e
|
|||
|
5a18f14929
|
|||
|
f12880688d
|
|||
|
bb5bbfe16a
|
|||
|
427e1ca37c
|
|||
|
96fdd9ecc5
|
|||
|
02771b655b
|
|||
|
d1c8d2c39b
|
|||
|
0efd742e8a
|
|||
|
ae1fe638d5
|
|||
|
445d95023b
|
|||
|
fc66f0bb47
|
|||
|
2cd6b35bee
|
|||
|
09a216c6ec
|
|||
|
44d17325c2
|
|||
|
544ce77cbc
|
|||
|
63c3c30b23
|
|||
|
d23c4ecc7c
|
|||
|
a46656dff8
|
|||
|
77db153ff5
|
|||
|
520d95bc07
|
|||
|
451df3f4e7
|
|||
|
011fac15ed
|
|||
|
347682ad0b
|
|||
|
1a2b979add
|
|||
|
b1c90cc380
|
|||
|
3a66b8143a
|
|||
|
64bbd3aabd
|
|||
|
08799a13d0
|
|||
|
1aef9c3bbb
|
|||
|
1f38303747
|
|||
|
640777b00c
|
|||
|
1d657193cf
|
|||
|
bab5406295
|
|||
|
725ae7d64d
|
|||
|
37a0c3967e
|
|||
|
ea0692548f
|
|||
|
48ea23e648
|
|||
|
40320e4920
|
|||
|
3ca0f61632
|
|||
|
6ffaac96e3
|
|||
|
13c7713d0c
|
|||
|
42389f7ec5
|
|||
|
30f130c691
|
|||
|
ceb4d26087
|
|||
|
852f3a9b3d
|
|||
|
5e02dbdb0d
|
|||
|
6a3248d472
|
|||
|
67404c98d9
|
|||
|
b9bf69cfce
|
|||
|
4648f98272
|
|||
|
11d99439ac
|
|||
|
39e4c5b8ac
|
|||
|
e8f6db38b6
|
|||
|
20d5b71575
|
|||
|
e903e7f542
|
|||
|
1caa051f4d
|
|||
|
dcdc6f7f6d
|
|||
|
5ad6f26b46
|
|||
|
7ba75a79f4
|
|||
|
9ef84d3904
|
|||
|
3b7b6e51fb
|
|||
|
b1b4debb82
|
|||
|
021cbbc2a8
|
|||
|
a4a54a4a4d
|
|||
|
04a344aac6
|
|||
|
6b98156a3d
|
|||
|
753432cf09
|
|||
|
f8902e3679
|
|||
|
8ee53a5164
|
|||
|
3981d44757
|
|||
|
9fd67e47b4
|
|||
|
4dcec40156
|
|||
|
9a274c78a3
|
|||
|
5647c3a91f
|
|||
|
992139c75d
|
|||
|
57c69b533e
|
|||
|
6f0c2a80f2
|
|||
|
08dfefb28d
|
|||
|
b081629662
|
|||
|
fba541f301
|
|||
|
5f0da3d5c2
|
|||
|
4d5841dd62
|
|||
|
9e752b588a
|
|||
|
27b1aaae38
|
|||
|
9e18de1dc2
|
|||
|
b80ea91a42
|
|||
|
30a9dfa4b8
|
|||
|
8d657b6fdf
|
|||
|
ae9b9adfd2
|
|||
|
dd6a480a21
|
|||
|
3942272c30
|
|||
|
9036986156
|
|||
|
a394971dd7
|
|||
|
9daba60809
|
|||
|
bcd79a22ff
|
|||
|
0ff7ab915b
|
|||
|
823575acac
|
|||
|
136bc0917b
|
|||
|
d6b082dd0b
|
|||
|
89d6d9576b
|
|||
|
fafce04a5d
|
|||
|
5d760a1db9
|
|||
|
d197e40b2a
|
|||
|
2008902247
|
|||
|
30ac985fd2
|
|||
|
e9fec368f8
|
|||
|
46add42f58
|
|||
|
377b61e342
|
|||
|
520c36db6d
|
|||
|
3352bb975b
|
|||
|
f7f48d57e9
|
|||
|
5c2345128e
|
|||
|
78f9676b1f
|
|||
|
5b5b676132
|
|||
|
78383fb6e8
|
|||
|
e97f6a393f
|
|||
|
eeffefd22b
|
|||
|
ac825640ab
|
|||
|
a7f7ce1795
|
|||
|
38c639e35c
|
|||
|
b2cb13e94c
|
|||
|
46f98d12d6
|
|||
|
503c7f953c
|
|||
|
15c9f6545d
|
|||
|
83b0e32c55
|
|||
|
eeaf26e7a2
|
|||
|
b587caf2e8
|
|||
|
f1c2ca4928
|
|||
|
0ca301219f
|
|||
|
e2199e1276
|
|||
|
86eacb3208
|
|||
|
8541bdd858
|
|||
|
46be0b0dc8
|
|||
|
cbe37e87e7
|
|||
|
66d741fb07
|
|||
|
0d449011f6
|
|||
|
46428ed85d
|
|||
|
081d6b463c
|
|||
|
11b3171180
|
|||
|
adbb84c3dd
|
|||
|
1084e31d95
|
|||
|
27a1b8fe0a
|
|||
|
b2141a41d7
|
|||
|
c0dff5bc87
|
|||
|
04513c0510
|
|||
|
28ebf973d6
|
|||
|
41aeb404ec
|
|||
|
0b1009786f
|
|||
|
b390640376
|
|||
|
ad2c9f36cd
|
|||
|
67db3fbb8d
|
|||
|
560cb626a1
|
|||
|
c33a6a5b7e
|
|||
|
952082bd9b
|
|||
|
24a9b24823
|
|||
|
c2e61e7987
|
|||
|
86787b3bc5
|
|||
|
cdfcfe6ce0
|
|||
|
68a2f0c240
|
|||
|
7319c7adf9
|
|||
|
e9c890cbb2
|
|||
|
6f924336fc
|
|||
|
bd88f10524
|
|||
|
e34e3b917e
|
|||
|
b0ba165107
|
|||
|
351d6c5a35
|
|||
|
f23f73701c
|
|||
|
876917229a
|
|||
|
0558032c2d
|
|||
|
c61cdc505f
|
|||
|
062edb3487
|
|||
|
e4355279a1
|
|||
|
289fdebead
|
|||
|
9c9e190db9
|
|||
|
d7d42c69a1
|
|||
|
c758e762bd
|
|||
|
10f8b1c221
|
|||
|
6907700d67
|
|||
|
0243f3ffbd
|
|||
|
cd0beeaf8e
|
|||
|
a69273ab2a
|
|||
|
4cd0f57e48
|
|||
|
33a0e6c01b
|
|||
|
d58f5c7590
|
|||
|
1da992e342
|
|||
|
9641805ec2
|
|||
|
0738f4889a
|
|||
|
7de3cfe221
|
|||
|
8b0648dd5d
|
|||
|
4667fac76c
|
|||
|
52e5443b0e
|
+6
-28
@@ -1,37 +1,15 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.pkg
|
||||
/hakurei
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
# produced by tools and text editors
|
||||
*.qcow2
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# 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
|
||||
|
||||
# release
|
||||
/dist/hakurei-*
|
||||
|
||||
# interactive nixos vm
|
||||
nixos.qcow2
|
||||
# cmd/dist default destination
|
||||
/dist
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
HAKUREI_DIST_MAKE='' exec "$(dirname -- "$0")/cmd/dist/dist.sh"
|
||||
+22
-23
@@ -2,7 +2,7 @@
|
||||
package check
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
@@ -30,6 +30,16 @@ func (e AbsoluteError) Is(target error) bool {
|
||||
// Absolute holds a pathname checked to be absolute.
|
||||
type Absolute struct{ pathname unique.Handle[string] }
|
||||
|
||||
var (
|
||||
_ encoding.TextAppender = new(Absolute)
|
||||
_ encoding.TextMarshaler = new(Absolute)
|
||||
_ encoding.TextUnmarshaler = new(Absolute)
|
||||
|
||||
_ encoding.BinaryAppender = new(Absolute)
|
||||
_ encoding.BinaryMarshaler = new(Absolute)
|
||||
_ encoding.BinaryUnmarshaler = new(Absolute)
|
||||
)
|
||||
|
||||
// ok returns whether [Absolute] is not the zero value.
|
||||
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
|
||||
|
||||
@@ -84,13 +94,16 @@ func (a *Absolute) Append(elem ...string) *Absolute {
|
||||
// Dir calls [filepath.Dir] with [Absolute] as its argument.
|
||||
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }
|
||||
|
||||
// GobEncode returns the checked pathname.
|
||||
func (a *Absolute) GobEncode() ([]byte, error) {
|
||||
return []byte(a.String()), nil
|
||||
// AppendText appends the checked pathname.
|
||||
func (a *Absolute) AppendText(data []byte) ([]byte, error) {
|
||||
return append(data, a.String()...), nil
|
||||
}
|
||||
|
||||
// GobDecode stores data if it represents an absolute pathname.
|
||||
func (a *Absolute) GobDecode(data []byte) error {
|
||||
// MarshalText returns the checked pathname.
|
||||
func (a *Absolute) MarshalText() ([]byte, error) { return a.AppendText(nil) }
|
||||
|
||||
// UnmarshalText stores data if it represents an absolute pathname.
|
||||
func (a *Absolute) UnmarshalText(data []byte) error {
|
||||
pathname := string(data)
|
||||
if !filepath.IsAbs(pathname) {
|
||||
return AbsoluteError(pathname)
|
||||
@@ -99,23 +112,9 @@ func (a *Absolute) GobDecode(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON returns a JSON representation of the checked pathname.
|
||||
func (a *Absolute) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(a.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON stores data if it represents an absolute pathname.
|
||||
func (a *Absolute) UnmarshalJSON(data []byte) error {
|
||||
var pathname string
|
||||
if err := json.Unmarshal(data, &pathname); err != nil {
|
||||
return err
|
||||
}
|
||||
if !filepath.IsAbs(pathname) {
|
||||
return AbsoluteError(pathname)
|
||||
}
|
||||
a.pathname = unique.Make(pathname)
|
||||
return nil
|
||||
}
|
||||
func (a *Absolute) AppendBinary(data []byte) ([]byte, error) { return a.AppendText(data) }
|
||||
func (a *Absolute) MarshalBinary() ([]byte, error) { return a.MarshalText() }
|
||||
func (a *Absolute) UnmarshalBinary(data []byte) error { return a.UnmarshalText(data) }
|
||||
|
||||
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
|
||||
func SortAbs(x []*Absolute) {
|
||||
|
||||
+6
-15
@@ -170,20 +170,20 @@ func TestCodecAbsolute(t *testing.T) {
|
||||
|
||||
{"good", MustAbs("/etc"),
|
||||
nil,
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00",
|
||||
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00",
|
||||
|
||||
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
|
||||
{"not absolute", nil,
|
||||
AbsoluteError("etc"),
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
|
||||
`"etc"`, `{"val":"etc","magic":3236757504}`},
|
||||
{"zero", nil,
|
||||
new(AbsoluteError),
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
`""`, `{"val":"","magic":3236757504}`},
|
||||
}
|
||||
|
||||
@@ -347,15 +347,6 @@ func TestCodecAbsolute(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("json passthrough", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wantErr := "invalid character ':' looking for beginning of value"
|
||||
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
|
||||
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAbsoluteWrap(t *testing.T) {
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
+10
@@ -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
|
||||
Vendored
+249
@@ -0,0 +1,249 @@
|
||||
//go:build dist
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/sha512"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// mustRun runs a command with the current process's environment and panics
|
||||
// on error or non-zero exit code.
|
||||
func mustRun(ctx context.Context, name string, arg ...string) {
|
||||
cmd := exec.CommandContext(ctx, name, arg...)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed comp/_hakurei
|
||||
var comp []byte
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("")
|
||||
|
||||
verbose := os.Getenv("VERBOSE") != ""
|
||||
runTests := os.Getenv("HAKUREI_DIST_MAKE") == ""
|
||||
version := getenv("HAKUREI_VERSION", "untagged")
|
||||
prefix := getenv("PREFIX", "/usr")
|
||||
destdir := getenv("DESTDIR", "dist")
|
||||
|
||||
if verbose {
|
||||
log.Println()
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(destdir, 0755); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s, err := os.MkdirTemp(destdir, ".dist.*")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
var code int
|
||||
|
||||
if err = os.RemoveAll(s); err != nil {
|
||||
code = 1
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
if r := recover(); r != nil {
|
||||
code = 1
|
||||
log.Println(r)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}()
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
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",
|
||||
verboseFlag, "-o", s,
|
||||
"-ldflags=-s -w "+
|
||||
"-buildid= -linkmode external -extldflags=-static "+
|
||||
"-X hakurei.app/internal/info.buildVersion="+version+" "+
|
||||
"-X hakurei.app/internal/info.hakureiPath="+prefix+"/bin/hakurei "+
|
||||
"-X hakurei.app/internal/info.hsuPath="+prefix+"/bin/hsu "+
|
||||
"-X main.hakureiPath="+prefix+"/bin/hakurei",
|
||||
"./...",
|
||||
)
|
||||
log.Println()
|
||||
|
||||
if runTests {
|
||||
log.Println("##### Testing Hakurei.")
|
||||
mustRun(
|
||||
ctx, "go", "test",
|
||||
"-ldflags=-buildid= -linkmode external -extldflags=-static",
|
||||
"./...",
|
||||
)
|
||||
log.Println()
|
||||
}
|
||||
|
||||
log.Println("##### Creating distribution.")
|
||||
const suffix = ".tar.gz"
|
||||
distName := "hakurei-" + version + "-" + runtime.GOARCH
|
||||
var f *os.File
|
||||
if f, err = os.OpenFile(
|
||||
filepath.Join(s, distName+suffix),
|
||||
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
|
||||
0644,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
h := sha512.New()
|
||||
gw, _ := gzip.NewWriterLevel(io.MultiWriter(f, h), gzip.BestCompression)
|
||||
tw := tar.NewWriter(gw)
|
||||
|
||||
mustWriteHeader := func(name string, size int64, mode os.FileMode) {
|
||||
header := tar.Header{
|
||||
Name: filepath.Join(distName, name),
|
||||
Size: size,
|
||||
Mode: int64(mode),
|
||||
Uname: "root",
|
||||
Gname: "root",
|
||||
}
|
||||
|
||||
if mode&os.ModeDir != 0 {
|
||||
header.Typeflag = tar.TypeDir
|
||||
fmt.Printf("%s %s\n", mode, name)
|
||||
} else {
|
||||
header.Typeflag = tar.TypeReg
|
||||
fmt.Printf("%s %s (%d bytes)\n", mode, name, size)
|
||||
}
|
||||
|
||||
if err = tw.WriteHeader(&header); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
mustWriteFile := func(name string, data []byte, mode os.FileMode) {
|
||||
mustWriteHeader(name, int64(len(data)), mode)
|
||||
if mode&os.ModeDir != 0 {
|
||||
return
|
||||
}
|
||||
if _, err = tw.Write(data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
mustWriteFromPath := func(dst, src string, mode os.FileMode) {
|
||||
var r *os.File
|
||||
if r, err = os.Open(src); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var fi os.FileInfo
|
||||
if fi, err = r.Stat(); err != nil {
|
||||
_ = r.Close()
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if mode == 0 {
|
||||
mode = fi.Mode()
|
||||
}
|
||||
|
||||
mustWriteHeader(dst, fi.Size(), mode)
|
||||
if _, err = io.Copy(tw, r); err != nil {
|
||||
_ = r.Close()
|
||||
panic(err)
|
||||
} else if err = r.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
mustWriteFile(".", nil, fs.ModeDir|0755)
|
||||
mustWriteFile("comp/", nil, os.ModeDir|0755)
|
||||
mustWriteFile("comp/_hakurei", comp, 0644)
|
||||
mustWriteFile("install.sh", []byte(`#!/bin/sh -e
|
||||
cd "$(dirname -- "$0")" || exit 1
|
||||
|
||||
install -vDm0755 "bin/hakurei" "${DESTDIR}`+prefix+`/bin/hakurei"
|
||||
install -vDm0755 "bin/sharefs" "${DESTDIR}`+prefix+`/bin/sharefs"
|
||||
|
||||
install -vDm4511 "bin/hsu" "${DESTDIR}`+prefix+`/bin/hsu"
|
||||
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
|
||||
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
|
||||
fi
|
||||
|
||||
install -vDm0644 "comp/_hakurei" "${DESTDIR}`+prefix+`/share/zsh/site-functions/_hakurei"
|
||||
`), 0755)
|
||||
|
||||
mustWriteFromPath("README.md", "README.md", 0)
|
||||
mustWriteFile("hsurc.default", []byte("1000 0"), 0400)
|
||||
mustWriteFromPath("bin/hsu", filepath.Join(s, "hsu"), 04511)
|
||||
for _, name := range []string{
|
||||
"hakurei",
|
||||
"sharefs",
|
||||
} {
|
||||
mustWriteFromPath(
|
||||
filepath.Join("bin", name),
|
||||
filepath.Join(s, name),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
if err = tw.Close(); err != nil {
|
||||
panic(err)
|
||||
} else if err = gw.Close(); err != nil {
|
||||
panic(err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f = nil
|
||||
|
||||
if err = os.WriteFile(
|
||||
filepath.Join(destdir, distName+suffix+".sha512"),
|
||||
append(hex.AppendEncode(nil, h.Sum(nil)), " "+distName+suffix+"\n"...),
|
||||
0644,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = os.Rename(
|
||||
filepath.Join(s, distName+suffix),
|
||||
filepath.Join(destdir, distName+suffix),
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
+11
-4
@@ -39,6 +39,7 @@ var errSuccess = errors.New("success")
|
||||
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagInsecure bool
|
||||
flagJSON bool
|
||||
)
|
||||
c := command.New(out, log.Printf, "hakurei", func([]string) error {
|
||||
@@ -57,6 +58,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
return nil
|
||||
}).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
||||
Flag(&flagInsecure, "insecure", command.BoolFlag(false), "Allow use of insecure compatibility options").
|
||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
||||
|
||||
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
|
||||
@@ -75,7 +77,12 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
config.Container.Args = append(config.Container.Args, args[1:]...)
|
||||
}
|
||||
|
||||
outcome.Main(ctx, msg, config, flagIdentifierFile)
|
||||
var flags int
|
||||
if flagInsecure {
|
||||
flags |= hst.VAllowInsecure
|
||||
}
|
||||
|
||||
outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
|
||||
@@ -145,7 +152,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
}
|
||||
}
|
||||
|
||||
var et hst.Enablement
|
||||
var et hst.Enablements
|
||||
if flagWayland {
|
||||
et |= hst.EWayland
|
||||
}
|
||||
@@ -163,7 +170,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
ID: flagID,
|
||||
Identity: flagIdentity,
|
||||
Groups: flagGroups,
|
||||
Enablements: hst.NewEnablements(et),
|
||||
Enablements: &et,
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
@@ -282,7 +289,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
}
|
||||
}
|
||||
|
||||
outcome.Main(ctx, msg, &config, -1)
|
||||
outcome.Main(ctx, msg, &config, 0, -1)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestHelp(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"main", []string{}, `
|
||||
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
||||
Usage: hakurei [-h | --help] [-v] [--insecure] [--json] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
run Load and start container from configuration file
|
||||
|
||||
@@ -56,7 +56,7 @@ func printShowInstance(
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
if err := config.Validate(hst.VAllowInsecure); err != nil {
|
||||
valid = false
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
mustPrint(output, "Error: "+m+"!\n\n")
|
||||
|
||||
@@ -32,7 +32,7 @@ var (
|
||||
PID: 0xbeef,
|
||||
ShimPID: 0xcafe,
|
||||
Config: &hst.Config{
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EPipeWire),
|
||||
Enablements: new(hst.EWayland | hst.EPipeWire),
|
||||
Identity: 1,
|
||||
Container: &hst.ContainerConfig{
|
||||
Shell: check.MustAbs("/bin/sh"),
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// cache refers to an instance of [pkg.Cache] that might be open.
|
||||
type cache struct {
|
||||
ctx context.Context
|
||||
msg message.Msg
|
||||
|
||||
// Should generally not be used directly.
|
||||
c *pkg.Cache
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// open opens the underlying [pkg.Cache].
|
||||
func (cache *cache) open() (err error) {
|
||||
if cache.c != nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
var base *check.Absolute
|
||||
if cache.base, err = filepath.Abs(cache.base); err != nil {
|
||||
return
|
||||
} else if base, err = check.NewAbs(cache.base); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var flags int
|
||||
if cache.idle {
|
||||
flags |= pkg.CSchedIdle
|
||||
}
|
||||
if cache.hostAbstract {
|
||||
flags |= pkg.CHostAbstract
|
||||
}
|
||||
if !cache.verboseInit {
|
||||
flags |= pkg.CSuppressInit
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-cache.ctx.Done():
|
||||
if testing.Testing() {
|
||||
return
|
||||
}
|
||||
os.Exit(2)
|
||||
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
cache.msg.Verbosef("opening cache at %s", base)
|
||||
cache.c, err = pkg.Open(
|
||||
cache.ctx,
|
||||
cache.msg,
|
||||
flags,
|
||||
cache.cures,
|
||||
cache.jobs,
|
||||
base,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
done <- struct{}{}
|
||||
|
||||
if cache.qemu != nil {
|
||||
var pathname *check.Absolute
|
||||
pathname, _, err = cache.c.Cure(cache.qemu)
|
||||
if err != nil {
|
||||
cache.c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
pkg.RegisterArch("riscv64", container.BinfmtEntry{
|
||||
Offset: 0,
|
||||
Magic: "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00",
|
||||
Mask: "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
|
||||
Interpreter: pathname.Append(
|
||||
"system/bin",
|
||||
"qemu-riscv64",
|
||||
),
|
||||
})
|
||||
pkg.RegisterArch("arm64", container.BinfmtEntry{
|
||||
Offset: 0,
|
||||
Magic: "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00",
|
||||
Mask: "\xff\xff\xff\xff\xff\xff\xff\xfc\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
|
||||
Interpreter: pathname.Append(
|
||||
"system/bin",
|
||||
"qemu-aarch64",
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Close closes the underlying [pkg.Cache] if it is open.
|
||||
func (cache *cache) Close() {
|
||||
if cache.c != nil {
|
||||
cache.c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Do calls f on the underlying cache and returns its error value.
|
||||
func (cache *cache) Do(f func(cache *pkg.Cache) error) error {
|
||||
if cache.c == nil {
|
||||
if err := cache.open(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return f(cache.c)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cm := cache{
|
||||
ctx: t.Context(),
|
||||
msg: message.New(log.New(os.Stderr, "check: ", 0)),
|
||||
base: t.TempDir(),
|
||||
|
||||
hostAbstract: true, idle: true,
|
||||
}
|
||||
defer cm.Close()
|
||||
cm.Close()
|
||||
|
||||
if err := cm.open(); err != nil {
|
||||
t.Fatalf("open: error = %v", err)
|
||||
}
|
||||
if err := cm.open(); err != os.ErrInvalid {
|
||||
t.Errorf("(duplicate) open: error = %v", err)
|
||||
}
|
||||
|
||||
if err := cm.Do(func(cache *pkg.Cache) error {
|
||||
return cache.Scrub(0)
|
||||
}); err != nil {
|
||||
t.Errorf("Scrub: error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
"unique"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/internal/pkg"
|
||||
)
|
||||
|
||||
// daemonTimeout is the maximum amount of time cureFromIR will wait on I/O.
|
||||
const daemonTimeout = 30 * time.Second
|
||||
|
||||
// daemonDeadline returns the deadline corresponding to daemonTimeout, or the
|
||||
// zero value when running in a test.
|
||||
func daemonDeadline() time.Time {
|
||||
if testing.Testing() {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Now().Add(daemonTimeout)
|
||||
}
|
||||
|
||||
const (
|
||||
// remoteNoReply notifies that the client will not receive a cure reply.
|
||||
remoteNoReply = 1 << iota
|
||||
)
|
||||
|
||||
// cureFromIR services an IR curing request.
|
||||
func cureFromIR(
|
||||
cache *pkg.Cache,
|
||||
conn net.Conn,
|
||||
flags uint64,
|
||||
) (pkg.Artifact, error) {
|
||||
a, decodeErr := cache.NewDecoder(conn).Decode()
|
||||
if decodeErr != nil {
|
||||
_, err := conn.Write([]byte("\x00" + decodeErr.Error()))
|
||||
return nil, errors.Join(decodeErr, err, conn.Close())
|
||||
}
|
||||
|
||||
pathname, _, cureErr := cache.Cure(a)
|
||||
if flags&remoteNoReply != 0 {
|
||||
return a, errors.Join(cureErr, conn.Close())
|
||||
}
|
||||
if err := conn.SetWriteDeadline(daemonDeadline()); err != nil {
|
||||
return a, errors.Join(cureErr, err, conn.Close())
|
||||
}
|
||||
if cureErr != nil {
|
||||
_, err := conn.Write([]byte("\x00" + cureErr.Error()))
|
||||
return a, errors.Join(cureErr, err, conn.Close())
|
||||
}
|
||||
_, err := conn.Write([]byte(pathname.String()))
|
||||
if testing.Testing() && errors.Is(err, io.ErrClosedPipe) {
|
||||
return a, nil
|
||||
}
|
||||
return a, errors.Join(err, conn.Close())
|
||||
}
|
||||
|
||||
const (
|
||||
// specialCancel is a message consisting of a single identifier referring
|
||||
// to a curing artifact to be cancelled.
|
||||
specialCancel = iota
|
||||
// specialAbort requests for all pending cures to be aborted. It has no
|
||||
// message body.
|
||||
specialAbort
|
||||
|
||||
// remoteSpecial denotes a special message with custom layout.
|
||||
remoteSpecial = math.MaxUint64
|
||||
)
|
||||
|
||||
// writeSpecialHeader writes the header of a remoteSpecial message.
|
||||
func writeSpecialHeader(conn net.Conn, kind uint64) error {
|
||||
var sh [16]byte
|
||||
binary.LittleEndian.PutUint64(sh[:], remoteSpecial)
|
||||
binary.LittleEndian.PutUint64(sh[8:], kind)
|
||||
if n, err := conn.Write(sh[:]); err != nil {
|
||||
return err
|
||||
} else if n != len(sh) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cancelIdent reads an identifier from conn and cancels the corresponding cure.
|
||||
func cancelIdent(
|
||||
cache *pkg.Cache,
|
||||
conn net.Conn,
|
||||
) (*pkg.ID, bool, error) {
|
||||
var ident pkg.ID
|
||||
if _, err := io.ReadFull(conn, ident[:]); err != nil {
|
||||
return nil, false, errors.Join(err, conn.Close())
|
||||
}
|
||||
ok := cache.Cancel(unique.Make(ident))
|
||||
return &ident, ok, conn.Close()
|
||||
}
|
||||
|
||||
// serve services connections from a [net.UnixListener].
|
||||
func serve(
|
||||
ctx context.Context,
|
||||
log *log.Logger,
|
||||
cm *cache,
|
||||
ul *net.UnixListener,
|
||||
) error {
|
||||
ul.SetUnlinkOnClose(true)
|
||||
if cm.c == nil {
|
||||
if err := cm.open(); err != nil {
|
||||
return errors.Join(err, ul.Close())
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
wg.Go(func() {
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
|
||||
conn, err := ul.AcceptUnix()
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
log.Println(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
wg.Go(func() {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = conn.SetDeadline(time.Now())
|
||||
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
if _err := conn.SetReadDeadline(daemonDeadline()); _err != nil {
|
||||
log.Println(_err)
|
||||
if _err = conn.Close(); _err != nil {
|
||||
log.Println(_err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var word [8]byte
|
||||
if _, _err := io.ReadFull(conn, word[:]); _err != nil {
|
||||
log.Println(_err)
|
||||
if _err = conn.Close(); _err != nil {
|
||||
log.Println(_err)
|
||||
}
|
||||
return
|
||||
}
|
||||
flags := binary.LittleEndian.Uint64(word[:])
|
||||
|
||||
if flags == remoteSpecial {
|
||||
if _, _err := io.ReadFull(conn, word[:]); _err != nil {
|
||||
log.Println(_err)
|
||||
if _err = conn.Close(); _err != nil {
|
||||
log.Println(_err)
|
||||
}
|
||||
return
|
||||
}
|
||||
switch special := binary.LittleEndian.Uint64(word[:]); special {
|
||||
default:
|
||||
log.Printf("invalid special %d", special)
|
||||
|
||||
case specialCancel:
|
||||
if id, ok, _err := cancelIdent(cm.c, conn); _err != nil {
|
||||
log.Println(_err)
|
||||
} else if !ok {
|
||||
log.Println(
|
||||
"attempting to cancel invalid artifact",
|
||||
pkg.Encode(*id),
|
||||
)
|
||||
} else {
|
||||
log.Println(
|
||||
"cancelled artifact",
|
||||
pkg.Encode(*id),
|
||||
)
|
||||
}
|
||||
|
||||
case specialAbort:
|
||||
log.Println("aborting all pending cures")
|
||||
cm.c.Abort()
|
||||
if _err := conn.Close(); _err != nil {
|
||||
log.Println(_err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if a, _err := cureFromIR(cm.c, conn, flags); _err != nil {
|
||||
log.Println(_err)
|
||||
} else {
|
||||
log.Printf(
|
||||
"fulfilled artifact %s",
|
||||
pkg.Encode(cm.c.Ident(a).Value()),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
<-ctx.Done()
|
||||
if err := ul.SetDeadline(time.Now()); err != nil {
|
||||
return errors.Join(err, ul.Close())
|
||||
}
|
||||
wg.Wait()
|
||||
return ul.Close()
|
||||
}
|
||||
|
||||
// dial wraps [net.DialUnix] with a context.
|
||||
func dial(ctx context.Context, addr *net.UnixAddr) (
|
||||
done chan<- struct{},
|
||||
conn *net.UnixConn,
|
||||
err error,
|
||||
) {
|
||||
conn, err = net.DialUnix("unix", nil, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
d := make(chan struct{})
|
||||
done = d
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = conn.SetDeadline(time.Now())
|
||||
|
||||
case <-d:
|
||||
return
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
// cureRemote cures a [pkg.Artifact] on a daemon.
|
||||
func cureRemote(
|
||||
ctx context.Context,
|
||||
addr *net.UnixAddr,
|
||||
a pkg.Artifact,
|
||||
flags uint64,
|
||||
) (*check.Absolute, error) {
|
||||
if flags == remoteSpecial {
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
|
||||
done, conn, err := dial(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer close(done)
|
||||
|
||||
if n, flagErr := conn.Write(binary.LittleEndian.AppendUint64(nil, flags)); flagErr != nil {
|
||||
return nil, errors.Join(flagErr, conn.Close())
|
||||
} else if n != 8 {
|
||||
return nil, errors.Join(io.ErrShortWrite, conn.Close())
|
||||
}
|
||||
|
||||
if err = pkg.NewIR().EncodeAll(conn, a); err != nil {
|
||||
return nil, errors.Join(err, conn.Close())
|
||||
} else if err = conn.CloseWrite(); err != nil {
|
||||
return nil, errors.Join(err, conn.Close())
|
||||
}
|
||||
|
||||
if flags&remoteNoReply != 0 {
|
||||
return nil, conn.Close()
|
||||
}
|
||||
|
||||
payload, recvErr := io.ReadAll(conn)
|
||||
if err = errors.Join(recvErr, conn.Close()); err != nil {
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||
err = cancelErr
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(payload) > 0 && payload[0] == 0 {
|
||||
return nil, errors.New(string(payload[1:]))
|
||||
}
|
||||
|
||||
var p *check.Absolute
|
||||
p, err = check.NewAbs(string(payload))
|
||||
return p, err
|
||||
}
|
||||
|
||||
// cancelRemote cancels a [pkg.Artifact] curing on a daemon.
|
||||
func cancelRemote(
|
||||
ctx context.Context,
|
||||
addr *net.UnixAddr,
|
||||
a pkg.Artifact,
|
||||
wait bool,
|
||||
) error {
|
||||
done, conn, err := dial(ctx, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close(done)
|
||||
|
||||
if err = writeSpecialHeader(conn, specialCancel); err != nil {
|
||||
return errors.Join(err, conn.Close())
|
||||
}
|
||||
|
||||
var n int
|
||||
id := pkg.NewIR().Ident(a).Value()
|
||||
if n, err = conn.Write(id[:]); err != nil {
|
||||
return errors.Join(err, conn.Close())
|
||||
} else if n != len(id) {
|
||||
return errors.Join(io.ErrShortWrite, 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 {
|
||||
return err
|
||||
}
|
||||
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())
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestNoReply(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !daemonDeadline().IsZero() {
|
||||
t.Fatal("daemonDeadline did not return the zero value")
|
||||
}
|
||||
|
||||
c, err := pkg.Open(
|
||||
t.Context(),
|
||||
message.New(log.New(os.Stderr, "cir: ", 0)),
|
||||
0, 0, 0,
|
||||
check.MustAbs(t.TempDir()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: error = %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
client, server := net.Pipe()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
go func() {
|
||||
<-t.Context().Done()
|
||||
if _err := client.SetDeadline(time.Now()); _err != nil && !errors.Is(_err, io.ErrClosedPipe) {
|
||||
panic(_err)
|
||||
}
|
||||
}()
|
||||
|
||||
if _err := c.EncodeAll(
|
||||
client,
|
||||
pkg.NewFile("check", []byte{0}),
|
||||
); _err != nil {
|
||||
panic(_err)
|
||||
} else if _err = client.Close(); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}()
|
||||
|
||||
a, cureErr := cureFromIR(c, server, remoteNoReply)
|
||||
if cureErr != nil {
|
||||
t.Fatalf("cureFromIR: error = %v", cureErr)
|
||||
}
|
||||
|
||||
<-done
|
||||
wantIdent := pkg.MustDecode("fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG")
|
||||
if gotIdent := c.Ident(a).Value(); gotIdent != wantIdent {
|
||||
t.Errorf(
|
||||
"cureFromIR: %s, want %s",
|
||||
pkg.Encode(gotIdent), pkg.Encode(wantIdent),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemon(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "daemon: ", 0)
|
||||
|
||||
addr := net.UnixAddr{
|
||||
Name: filepath.Join(t.TempDir(), "daemon"),
|
||||
Net: "unix",
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
cm := cache{
|
||||
ctx: ctx,
|
||||
msg: message.New(logger),
|
||||
base: t.TempDir(),
|
||||
}
|
||||
defer cm.Close()
|
||||
|
||||
ul, err := net.ListenUnix("unix", &addr)
|
||||
if err != nil {
|
||||
t.Fatalf("ListenUnix: error = %v", err)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
if _err := serve(ctx, logger, &cm, ul); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = cancelRemote(ctx, &addr, pkg.NewFile("nonexistent", nil), true); err != nil {
|
||||
t.Fatalf("cancelRemote: error = %v", err)
|
||||
}
|
||||
|
||||
if err = abortRemote(ctx, &addr, true); err != nil {
|
||||
t.Fatalf("abortRemote: error = %v", err)
|
||||
}
|
||||
|
||||
// keep this last for synchronisation
|
||||
var p *check.Absolute
|
||||
p, err = cureRemote(ctx, &addr, pkg.NewFile("check", []byte{0}), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("cureRemote: error = %v", err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
<-done
|
||||
|
||||
const want = "fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG"
|
||||
if got := filepath.Base(p.String()); got != want {
|
||||
t.Errorf("cureRemote: %s, want %s", got, want)
|
||||
}
|
||||
|
||||
wantLog := []string{
|
||||
"",
|
||||
"daemon: aborting all pending cures",
|
||||
"daemon: attempting to cancel invalid artifact kQm9fmnCmXST1-MMmxzcau2oKZCXXrlZydo4PkeV5hO_2PKfeC8t98hrbV_ZZx_j",
|
||||
"daemon: fulfilled artifact fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG",
|
||||
}
|
||||
gotLog := strings.Split(buf.String(), "\n")
|
||||
slices.Sort(gotLog)
|
||||
if !slices.Equal(gotLog, wantLog) {
|
||||
t.Errorf(
|
||||
"serve: logged\n%s\nwant\n%s",
|
||||
strings.Join(gotLog, "\n"), strings.Join(wantLog, "\n"),
|
||||
)
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
// commandInfo implements the info subcommand.
|
||||
func commandInfo(
|
||||
cm *cache,
|
||||
args []string,
|
||||
w io.Writer,
|
||||
writeStatus bool,
|
||||
r *rosa.Report,
|
||||
) (err error) {
|
||||
if len(args) == 0 {
|
||||
return errors.New("info requires at least 1 argument")
|
||||
}
|
||||
|
||||
// recovered by HandleAccess
|
||||
mustPrintln := func(a ...any) {
|
||||
if _, _err := fmt.Fprintln(w, a...); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}
|
||||
mustPrint := func(a ...any) {
|
||||
if _, _err := fmt.Fprint(w, a...); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}
|
||||
|
||||
for i, name := range args {
|
||||
if p, ok := rosa.ResolveName(name); !ok {
|
||||
return fmt.Errorf("unknown artifact %q", name)
|
||||
} else {
|
||||
var suffix string
|
||||
if version := rosa.Std.Version(p); version != rosa.Unversioned {
|
||||
suffix += "-" + version
|
||||
}
|
||||
mustPrintln("name : " + name + suffix)
|
||||
|
||||
meta := rosa.GetMetadata(p)
|
||||
mustPrintln("description : " + meta.Description)
|
||||
if meta.Website != "" {
|
||||
mustPrintln("website : " +
|
||||
strings.TrimSuffix(meta.Website, "/"))
|
||||
}
|
||||
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
|
||||
}
|
||||
mustPrint(" " + s)
|
||||
}
|
||||
mustPrintln()
|
||||
}
|
||||
|
||||
const statusPrefix = "status : "
|
||||
if writeStatus {
|
||||
if r == nil {
|
||||
var f io.ReadSeekCloser
|
||||
err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
f, err = cache.OpenStatus(rosa.Std.Load(p))
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
mustPrintln(
|
||||
statusPrefix + "not yet cured",
|
||||
)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
mustPrint(statusPrefix)
|
||||
_, err = io.Copy(w, f)
|
||||
if err = errors.Join(err, f.Close()); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p)))
|
||||
if status == nil {
|
||||
mustPrintln(
|
||||
statusPrefix + "not in report",
|
||||
)
|
||||
} else {
|
||||
mustPrintln("size :", n)
|
||||
mustPrint(statusPrefix)
|
||||
if _, err = w.Write(status); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if i != len(args)-1 {
|
||||
mustPrintln()
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
status map[string]string
|
||||
report string
|
||||
want string
|
||||
wantErr any
|
||||
}{
|
||||
{"qemu", []string{"qemu"}, nil, "", `
|
||||
name : qemu-` + rosa.Std.Version(rosa.QEMU) + `
|
||||
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) + `
|
||||
`, nil},
|
||||
|
||||
{"multi", []string{"hakurei", "hakurei-dist"}, nil, "", `
|
||||
name : hakurei-` + rosa.Std.Version(rosa.Hakurei) + `
|
||||
description : low-level userspace tooling for Rosa OS
|
||||
website : https://hakurei.app
|
||||
|
||||
name : hakurei-dist-` + rosa.Std.Version(rosa.HakureiDist) + `
|
||||
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) + `
|
||||
description : lossless data-compression library
|
||||
website : https://zlib.net
|
||||
|
||||
`, fmt.Errorf("unknown artifact %q", "\x00")},
|
||||
|
||||
{"status cache", []string{"zlib", "zstd"}, map[string]string{
|
||||
"zstd": "internal/pkg (amd64) on satori\n",
|
||||
"hakurei": "internal/pkg (amd64) on satori\n\n",
|
||||
}, "", `
|
||||
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
|
||||
description : lossless data-compression library
|
||||
website : https://zlib.net
|
||||
status : not yet cured
|
||||
|
||||
name : zstd-` + rosa.Std.Version(rosa.Zstd) + `
|
||||
description : a fast compression algorithm
|
||||
website : https://facebook.github.io/zstd
|
||||
status : internal/pkg (amd64) on satori
|
||||
`, nil},
|
||||
|
||||
{"status cache perm", []string{"zlib"}, map[string]string{
|
||||
"zlib": "\x00",
|
||||
}, "", `
|
||||
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
|
||||
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())),
|
||||
Err: syscall.EACCES,
|
||||
}
|
||||
}},
|
||||
|
||||
{"status report", []string{"zlib"}, nil, strings.Repeat("\x00", len(pkg.Checksum{})+8), `
|
||||
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
|
||||
description : lossless data-compression library
|
||||
website : https://zlib.net
|
||||
status : not in report
|
||||
`, nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
cm *cache
|
||||
buf strings.Builder
|
||||
r *rosa.Report
|
||||
)
|
||||
|
||||
if tc.status != nil || tc.report != "" {
|
||||
cm = &cache{
|
||||
ctx: context.Background(),
|
||||
msg: message.New(log.New(os.Stderr, "info: ", 0)),
|
||||
base: t.TempDir(),
|
||||
}
|
||||
defer cm.Close()
|
||||
}
|
||||
|
||||
if tc.report != "" {
|
||||
pathname := filepath.Join(t.TempDir(), "report")
|
||||
err := os.WriteFile(
|
||||
pathname,
|
||||
unsafe.Slice(unsafe.StringData(tc.report), len(tc.report)),
|
||||
0400,
|
||||
)
|
||||
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 {
|
||||
t.Fatalf("invalid name %q", name)
|
||||
}
|
||||
perm := os.FileMode(0400)
|
||||
if status == "\x00" {
|
||||
perm = 0
|
||||
}
|
||||
if err := cm.Do(func(cache *pkg.Cache) error {
|
||||
return os.WriteFile(filepath.Join(
|
||||
cm.base,
|
||||
"status",
|
||||
pkg.Encode(cache.Ident(rosa.Std.Load(p)).Value()),
|
||||
), unsafe.Slice(unsafe.StringData(status), len(status)), perm)
|
||||
}); err != nil {
|
||||
t.Fatalf("Do: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var wantErr error
|
||||
switch c := tc.wantErr.(type) {
|
||||
case error:
|
||||
wantErr = c
|
||||
case func(cm *cache) error:
|
||||
wantErr = c(cm)
|
||||
default:
|
||||
if tc.wantErr != nil {
|
||||
t.Fatalf("invalid wantErr %#v", tc.wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
if err := commandInfo(
|
||||
cm,
|
||||
tc.args,
|
||||
&buf,
|
||||
cm != nil,
|
||||
r,
|
||||
); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Fatalf("commandInfo: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
|
||||
if got := buf.String(); got != strings.TrimPrefix(tc.want, "\n") {
|
||||
t.Errorf("commandInfo:\n%s\nwant\n%s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 = int(rosa.PresetUnexportedStart)
|
||||
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(int(rosa.PresetUnexportedStart-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(int(rosa.PresetUnexportedStart-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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package pkgserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"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"`
|
||||
}{int(rosa.PresetUnexportedStart), 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",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("index", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkValidate(
|
||||
t, "limit=1&sort=0&index", 0, int(rosa.PresetUnexportedStart-1),
|
||||
"index must be an integer between 0 and "+strconv.Itoa(int(rosa.PresetUnexportedStart-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)),
|
||||
)
|
||||
})
|
||||
|
||||
checkWithSuffix := func(name, suffix string, want []*metadata) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newRequest(suffix)
|
||||
resp := w.Result()
|
||||
checkStatus(t, resp, http.StatusOK)
|
||||
checkAPIHeader(t, w.Header())
|
||||
checkPayloadFunc(t, resp, func(got *struct {
|
||||
Values []*metadata `json:"values"`
|
||||
}) bool {
|
||||
return slices.EqualFunc(got.Values, want, func(a, b *metadata) bool {
|
||||
return (a.Version == b.Version ||
|
||||
a.Version == rosa.Unversioned ||
|
||||
b.Version == rosa.Unversioned) &&
|
||||
a.HasReport == b.HasReport &&
|
||||
a.Name == b.Name &&
|
||||
a.Description == b.Description &&
|
||||
a.Website == b.Website
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
checkWithSuffix("declarationAscending", "?limit=2&index=1&sort=0", []*metadata{
|
||||
{
|
||||
Metadata: rosa.GetMetadata(1),
|
||||
Version: rosa.Std.Version(1),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(2),
|
||||
Version: rosa.Std.Version(2),
|
||||
},
|
||||
})
|
||||
checkWithSuffix("declarationAscending offset", "?limit=3&index=5&sort=0", []*metadata{
|
||||
{
|
||||
Metadata: rosa.GetMetadata(5),
|
||||
Version: rosa.Std.Version(5),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(6),
|
||||
Version: rosa.Std.Version(6),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(7),
|
||||
Version: rosa.Std.Version(7),
|
||||
},
|
||||
})
|
||||
checkWithSuffix("declarationDescending", "?limit=3&index=0&sort=1", []*metadata{
|
||||
{
|
||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 1),
|
||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 1),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 2),
|
||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 2),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 3),
|
||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 3),
|
||||
},
|
||||
})
|
||||
checkWithSuffix("declarationDescending offset", "?limit=1&index=37&sort=1", []*metadata{
|
||||
{
|
||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 38),
|
||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 38),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
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][rosa.PresetUnexportedStart]*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 {
|
||||
p rosa.PArtifact
|
||||
*rosa.Metadata
|
||||
|
||||
// Populated via [rosa.Toolchain.Version], [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
|
||||
}
|
||||
|
||||
var work [rosa.PresetUnexportedStart]*metadata
|
||||
index.names = make(map[string]*metadata)
|
||||
ir := pkg.NewIR()
|
||||
for p := range rosa.PresetUnexportedStart {
|
||||
m := metadata{
|
||||
p: p,
|
||||
|
||||
Metadata: rosa.GetMetadata(p),
|
||||
Version: rosa.Std.Version(p),
|
||||
}
|
||||
if m.Version == "" {
|
||||
return errors.New("invalid version from " + m.Name)
|
||||
}
|
||||
if m.Version == rosa.Unversioned {
|
||||
m.Version = ""
|
||||
}
|
||||
|
||||
if report != nil {
|
||||
id := ir.Ident(rosa.Std.Load(p))
|
||||
m.ids = pkg.Encode(id.Value())
|
||||
m.status, m.Size = report.ArtifactOf(id)
|
||||
m.HasReport = m.Size >= 0
|
||||
}
|
||||
|
||||
work[p] = &m
|
||||
index.names[m.Name] = &m
|
||||
}
|
||||
|
||||
index.sorts[declarationAscending] = work
|
||||
index.sorts[declarationDescending] = work
|
||||
slices.Reverse(index.sorts[declarationDescending][:])
|
||||
|
||||
index.sorts[nameAscending] = work
|
||||
slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
index.sorts[nameDescending] = index.sorts[nameAscending]
|
||||
slices.Reverse(index.sorts[nameDescending][:])
|
||||
|
||||
index.sorts[sizeAscending] = work
|
||||
slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int {
|
||||
return cmp.Compare(a.Size, b.Size)
|
||||
})
|
||||
index.sorts[sizeDescending] = index.sorts[sizeAscending]
|
||||
slices.Reverse(index.sorts[sizeDescending][:])
|
||||
|
||||
return
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 := 0; i < len(in); i++ {
|
||||
sum += in[i][1] - in[i][0]
|
||||
}
|
||||
return sum
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<!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">
|
||||
<title>Hakurei PkgServer</title>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hakurei PkgServer</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()">« Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next »</a></div>
|
||||
<table id="pkg-list">
|
||||
<tr><td>Loading...</td></tr>
|
||||
</table>
|
||||
<div class="page-controls"><a href="javascript:prevPage()">« Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next »</a></div>
|
||||
<footer>
|
||||
<p>©<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>
|
||||
@@ -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 "&"
|
||||
case '<':
|
||||
return "<"
|
||||
case '>':
|
||||
return ">"
|
||||
case '"':
|
||||
return """
|
||||
case "'":
|
||||
return "'"
|
||||
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()
|
||||
})
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}()
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !frontend
|
||||
|
||||
package ui
|
||||
|
||||
import "testing/fstest"
|
||||
|
||||
var static fstest.MapFS
|
||||
+291
-156
@@ -14,16 +14,18 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
@@ -40,6 +42,9 @@ import (
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
"hakurei.app/message"
|
||||
|
||||
"hakurei.app/cmd/mbf/internal/pkgserver"
|
||||
"hakurei.app/cmd/mbf/internal/pkgserver/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -53,65 +58,141 @@ func main() {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
var cache *pkg.Cache
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer stop()
|
||||
defer func() {
|
||||
if cache != nil {
|
||||
cache.Close()
|
||||
}
|
||||
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println(r)
|
||||
log.Fatal("consider scrubbing the on-disk cache")
|
||||
}
|
||||
}()
|
||||
var cm cache
|
||||
defer func() { cm.Close() }()
|
||||
|
||||
var (
|
||||
flagQuiet bool
|
||||
flagCures int
|
||||
flagBase string
|
||||
flagIdle bool
|
||||
)
|
||||
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) {
|
||||
msg.SwapVerbose(!flagQuiet)
|
||||
flagQEMU bool
|
||||
flagArch string
|
||||
flagCheck bool
|
||||
flagLTO bool
|
||||
|
||||
flagBase = os.ExpandEnv(flagBase)
|
||||
if flagBase == "" {
|
||||
flagBase = "cache"
|
||||
flagCrossOverride int
|
||||
|
||||
addr net.UnixAddr
|
||||
)
|
||||
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) error {
|
||||
msg.SwapVerbose(!flagQuiet)
|
||||
cm.ctx, cm.msg = ctx, msg
|
||||
cm.base = os.ExpandEnv(cm.base)
|
||||
if cm.base == "" {
|
||||
cm.base = "cache"
|
||||
}
|
||||
|
||||
var base *check.Absolute
|
||||
if flagBase, err = filepath.Abs(flagBase); err != nil {
|
||||
return
|
||||
} else if base, err = check.NewAbs(flagBase); err != nil {
|
||||
return
|
||||
addr.Net = "unix"
|
||||
addr.Name = os.ExpandEnv(addr.Name)
|
||||
if addr.Name == "" {
|
||||
addr.Name = filepath.Join(cm.base, "daemon")
|
||||
}
|
||||
|
||||
var flags int
|
||||
if flagIdle {
|
||||
flags &= pkg.CSchedIdle
|
||||
if !flagCheck {
|
||||
flags |= rosa.OptSkipCheck
|
||||
}
|
||||
if !flagLTO {
|
||||
flags |= rosa.OptLLVMNoLTO
|
||||
}
|
||||
rosa.DropCaches("", flags)
|
||||
cross := flagArch != "" && flagArch != runtime.GOARCH
|
||||
if flagQEMU || cross {
|
||||
cm.qemu = rosa.Std.Load(rosa.QEMU)
|
||||
}
|
||||
cache, err = pkg.Open(ctx, msg, flags, flagCures, base)
|
||||
|
||||
return
|
||||
if cross {
|
||||
if flagCrossOverride != -1 {
|
||||
flags = flagCrossOverride
|
||||
}
|
||||
|
||||
rosa.DropCaches(flagArch, flags)
|
||||
if !rosa.HasStage0() {
|
||||
return pkg.UnsupportedArchError(flagArch)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}).Flag(
|
||||
&flagQuiet,
|
||||
"q", command.BoolFlag(false),
|
||||
"Do not print cure messages",
|
||||
).Flag(
|
||||
&flagCures,
|
||||
&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),
|
||||
"Maximum number of dependencies to cure at any given time",
|
||||
).Flag(
|
||||
&flagBase,
|
||||
&cm.jobs,
|
||||
"jobs", command.IntFlag(0),
|
||||
"Preferred number of jobs to run, when applicable",
|
||||
).Flag(
|
||||
&cm.base,
|
||||
"d", command.StringFlag("$MBF_CACHE_DIR"),
|
||||
"Directory to store cured artifacts",
|
||||
).Flag(
|
||||
&flagIdle,
|
||||
&cm.idle,
|
||||
"sched-idle", command.BoolFlag(false),
|
||||
"Set SCHED_IDLE scheduling policy",
|
||||
).Flag(
|
||||
&cm.hostAbstract,
|
||||
"host-abstract", command.BoolFlag(
|
||||
os.Getenv("MBF_HOST_ABSTRACT") != "",
|
||||
),
|
||||
"Do not restrict networked cure containers from connecting to host "+
|
||||
"abstract UNIX sockets",
|
||||
).Flag(
|
||||
&addr.Name,
|
||||
"socket", command.StringFlag("$MBF_DAEMON_SOCKET"),
|
||||
"Pathname of socket to bind to",
|
||||
)
|
||||
|
||||
c.NewCommand(
|
||||
"checksum", "Compute checksum of data read from standard input",
|
||||
func([]string) error {
|
||||
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
|
||||
}
|
||||
log.Println(pkg.Encode(pkg.Checksum(h.Sum(nil))))
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
{
|
||||
@@ -125,7 +206,9 @@ func main() {
|
||||
if flagShifts < 0 || flagShifts > 31 {
|
||||
flagShifts = 12
|
||||
}
|
||||
return cm.Do(func(cache *pkg.Cache) error {
|
||||
return cache.Scrub(runtime.NumCPU() << flagShifts)
|
||||
})
|
||||
},
|
||||
).Flag(
|
||||
&flagShifts,
|
||||
@@ -136,6 +219,7 @@ func main() {
|
||||
|
||||
{
|
||||
var (
|
||||
flagBind string
|
||||
flagStatus bool
|
||||
flagReport string
|
||||
)
|
||||
@@ -143,9 +227,7 @@ func main() {
|
||||
"info",
|
||||
"Display out-of-band metadata of an artifact",
|
||||
func(args []string) (err error) {
|
||||
if len(args) == 0 {
|
||||
return errors.New("info requires at least 1 argument")
|
||||
}
|
||||
const shutdownTimeout = 15 * time.Second
|
||||
|
||||
var r *rosa.Report
|
||||
if flagReport != "" {
|
||||
@@ -160,84 +242,42 @@ func main() {
|
||||
defer r.HandleAccess(&err)()
|
||||
}
|
||||
|
||||
for i, name := range args {
|
||||
if p, ok := rosa.ResolveName(name); !ok {
|
||||
return fmt.Errorf("unknown artifact %q", name)
|
||||
} else {
|
||||
var suffix string
|
||||
if version := rosa.Std.Version(p); version != rosa.Unversioned {
|
||||
suffix += "-" + version
|
||||
}
|
||||
fmt.Println("name : " + name + suffix)
|
||||
|
||||
meta := rosa.GetMetadata(p)
|
||||
fmt.Println("description : " + meta.Description)
|
||||
if meta.Website != "" {
|
||||
fmt.Println("website : " +
|
||||
strings.TrimSuffix(meta.Website, "/"))
|
||||
}
|
||||
if len(meta.Dependencies) > 0 {
|
||||
fmt.Print("depends on :")
|
||||
for _, d := range meta.Dependencies {
|
||||
s := rosa.GetMetadata(d).Name
|
||||
if version := rosa.Std.Version(d); version != rosa.Unversioned {
|
||||
s += "-" + version
|
||||
}
|
||||
fmt.Print(" " + s)
|
||||
}
|
||||
fmt.Println()
|
||||
if flagBind == "" {
|
||||
return commandInfo(&cm, args, os.Stdout, flagStatus, r)
|
||||
}
|
||||
|
||||
const statusPrefix = "status : "
|
||||
if flagStatus {
|
||||
if r == nil {
|
||||
var f io.ReadSeekCloser
|
||||
f, err = cache.OpenStatus(rosa.Std.Load(p))
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
fmt.Println(
|
||||
statusPrefix + "not yet cured",
|
||||
)
|
||||
} else {
|
||||
var mux http.ServeMux
|
||||
ui.Register(&mux)
|
||||
if err = pkgserver.Register(ctx, &mux, r); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
fmt.Print(statusPrefix)
|
||||
_, err = io.Copy(os.Stdout, f)
|
||||
if err = errors.Join(err, f.Close()); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p)))
|
||||
if status == nil {
|
||||
fmt.Println(
|
||||
statusPrefix + "not in report",
|
||||
)
|
||||
} else {
|
||||
fmt.Println("size :", n)
|
||||
fmt.Print(statusPrefix)
|
||||
if _, err = os.Stdout.Write(status); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if i != len(args)-1 {
|
||||
fmt.Println()
|
||||
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 nil
|
||||
return
|
||||
},
|
||||
).
|
||||
Flag(
|
||||
).Flag(
|
||||
&flagBind,
|
||||
"bind", command.StringFlag(""),
|
||||
"TCP address for the server to listen on",
|
||||
).Flag(
|
||||
&flagStatus,
|
||||
"status", command.BoolFlag(false),
|
||||
"Display cure status if available",
|
||||
).
|
||||
Flag(
|
||||
).Flag(
|
||||
&flagReport,
|
||||
"report", command.StringFlag(""),
|
||||
"Load cure status from this report file instead of cache",
|
||||
@@ -275,7 +315,9 @@ func main() {
|
||||
if ext.Isatty(int(w.Fd())) {
|
||||
return errors.New("output appears to be a terminal")
|
||||
}
|
||||
return cm.Do(func(cache *pkg.Cache) error {
|
||||
return rosa.WriteReport(msg, w, cache)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -338,14 +380,26 @@ func main() {
|
||||
" package(s) are out of date"))
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}).
|
||||
Flag(
|
||||
}).Flag(
|
||||
&flagJobs,
|
||||
"j", command.IntFlag(32),
|
||||
"Maximum number of simultaneous connections",
|
||||
)
|
||||
}
|
||||
|
||||
c.NewCommand(
|
||||
"daemon",
|
||||
"Service artifact IR with Rosa OS extensions",
|
||||
func(args []string) error {
|
||||
ul, err := net.ListenUnix("unix", &addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("listening on pathname socket at %s", addr.Name)
|
||||
return serve(ctx, log.Default(), &cm, ul)
|
||||
},
|
||||
)
|
||||
|
||||
{
|
||||
var (
|
||||
flagGentoo string
|
||||
@@ -370,25 +424,37 @@ func main() {
|
||||
rosa.SetGentooStage3(flagGentoo, checksum)
|
||||
}
|
||||
|
||||
_, _, _, stage1 := (t - 2).NewLLVM()
|
||||
_, _, _, stage2 := (t - 1).NewLLVM()
|
||||
_, _, _, stage3 := t.NewLLVM()
|
||||
var (
|
||||
pathname *check.Absolute
|
||||
checksum [2]unique.Handle[pkg.Checksum]
|
||||
)
|
||||
|
||||
if pathname, _, err = cache.Cure(stage1); err != nil {
|
||||
return err
|
||||
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
pathname, _, err = cache.Cure(
|
||||
(t - 2).Load(rosa.LLVM),
|
||||
)
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
log.Println("stage1:", pathname)
|
||||
|
||||
if pathname, checksum[0], err = cache.Cure(stage2); err != nil {
|
||||
return err
|
||||
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
pathname, checksum[0], err = cache.Cure(
|
||||
(t - 1).Load(rosa.LLVM),
|
||||
)
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
log.Println("stage2:", pathname)
|
||||
if pathname, checksum[1], err = cache.Cure(stage3); err != nil {
|
||||
return err
|
||||
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
pathname, checksum[1], err = cache.Cure(
|
||||
t.Load(rosa.LLVM),
|
||||
)
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
log.Println("stage3:", pathname)
|
||||
|
||||
@@ -405,28 +471,28 @@ func main() {
|
||||
}
|
||||
|
||||
if flagStage0 {
|
||||
if pathname, _, err = cache.Cure(
|
||||
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
pathname, _, err = cache.Cure(
|
||||
t.Load(rosa.Stage0),
|
||||
); err != nil {
|
||||
return err
|
||||
)
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
log.Println(pathname)
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
).
|
||||
Flag(
|
||||
).Flag(
|
||||
&flagGentoo,
|
||||
"gentoo", command.StringFlag(""),
|
||||
"Bootstrap from a Gentoo stage3 tarball",
|
||||
).
|
||||
Flag(
|
||||
).Flag(
|
||||
&flagChecksum,
|
||||
"checksum", command.StringFlag(""),
|
||||
"Checksum of Gentoo stage3 tarball",
|
||||
).
|
||||
Flag(
|
||||
).Flag(
|
||||
&flagStage0,
|
||||
"stage0", command.BoolFlag(false),
|
||||
"Create bootstrap stage0 tarball",
|
||||
@@ -438,6 +504,11 @@ func main() {
|
||||
flagDump string
|
||||
flagEnter bool
|
||||
flagExport string
|
||||
flagRemote bool
|
||||
flagNoReply bool
|
||||
|
||||
flagBoot bool
|
||||
flagStd bool
|
||||
)
|
||||
c.NewCommand(
|
||||
"cure",
|
||||
@@ -451,9 +522,20 @@ func main() {
|
||||
return fmt.Errorf("unknown artifact %q", args[0])
|
||||
}
|
||||
|
||||
t := rosa.Std
|
||||
if flagBoot {
|
||||
t -= 2
|
||||
} else if flagStd {
|
||||
t -= 1
|
||||
}
|
||||
|
||||
switch {
|
||||
default:
|
||||
pathname, _, err := cache.Cure(rosa.Std.Load(p))
|
||||
var pathname *check.Absolute
|
||||
err := cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
pathname, _, err = cache.Cure(t.Load(p))
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -493,7 +575,7 @@ func main() {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = cache.EncodeAll(f, rosa.Std.Load(p)); err != nil {
|
||||
if err = pkg.NewIR().EncodeAll(f, rosa.Std.Load(p)); err != nil {
|
||||
_ = f.Close()
|
||||
return err
|
||||
}
|
||||
@@ -501,33 +583,76 @@ func main() {
|
||||
return f.Close()
|
||||
|
||||
case flagEnter:
|
||||
return cm.Do(func(cache *pkg.Cache) error {
|
||||
return cache.EnterExec(
|
||||
ctx,
|
||||
rosa.Std.Load(p),
|
||||
t.Load(p),
|
||||
true, os.Stdin, os.Stdout, os.Stderr,
|
||||
rosa.AbsSystem.Append("bin", "mksh"),
|
||||
"sh",
|
||||
)
|
||||
})
|
||||
|
||||
case flagRemote:
|
||||
var flags uint64
|
||||
if flagNoReply {
|
||||
flags |= remoteNoReply
|
||||
}
|
||||
a := t.Load(p)
|
||||
pathname, err := cureRemote(ctx, &addr, a, flags)
|
||||
if !flagNoReply && err == nil {
|
||||
log.Println(pathname)
|
||||
}
|
||||
|
||||
if errors.Is(err, context.Canceled) {
|
||||
cc, cancel := context.WithDeadline(context.Background(), daemonDeadline())
|
||||
defer cancel()
|
||||
|
||||
if _err := cancelRemote(cc, &addr, a, false); _err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
},
|
||||
).
|
||||
Flag(
|
||||
).Flag(
|
||||
&flagDump,
|
||||
"dump", command.StringFlag(""),
|
||||
"Write IR to specified pathname and terminate",
|
||||
).
|
||||
Flag(
|
||||
).Flag(
|
||||
&flagExport,
|
||||
"export", command.StringFlag(""),
|
||||
"Export cured artifact to specified pathname",
|
||||
).
|
||||
Flag(
|
||||
).Flag(
|
||||
&flagEnter,
|
||||
"enter", command.BoolFlag(false),
|
||||
"Enter cure container with an interactive shell",
|
||||
).Flag(
|
||||
&flagRemote,
|
||||
"daemon", command.BoolFlag(false),
|
||||
"Cure artifact on the daemon",
|
||||
).Flag(
|
||||
&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",
|
||||
)
|
||||
}
|
||||
|
||||
c.NewCommand(
|
||||
"abort",
|
||||
"Abort all pending cures on the daemon",
|
||||
func([]string) error { return abortRemote(ctx, &addr, false) },
|
||||
)
|
||||
|
||||
{
|
||||
var (
|
||||
flagNet bool
|
||||
@@ -539,7 +664,7 @@ func main() {
|
||||
"shell",
|
||||
"Interactive shell in the specified Rosa OS environment",
|
||||
func(args []string) error {
|
||||
presets := make([]rosa.PArtifact, len(args))
|
||||
presets := make([]rosa.PArtifact, len(args)+3)
|
||||
for i, arg := range args {
|
||||
p, ok := rosa.ResolveName(arg)
|
||||
if !ok {
|
||||
@@ -547,21 +672,24 @@ func main() {
|
||||
}
|
||||
presets[i] = p
|
||||
}
|
||||
|
||||
base := rosa.LLVM
|
||||
if !flagWithToolchain {
|
||||
base = rosa.Musl
|
||||
}
|
||||
presets = append(presets,
|
||||
base,
|
||||
rosa.Mksh,
|
||||
rosa.Toybox,
|
||||
)
|
||||
|
||||
root := make(pkg.Collect, 0, 6+len(args))
|
||||
root = rosa.Std.AppendPresets(root, presets...)
|
||||
|
||||
if flagWithToolchain {
|
||||
musl, compilerRT, runtimes, clang := (rosa.Std - 1).NewLLVM()
|
||||
root = append(root, musl, compilerRT, runtimes, clang)
|
||||
} else {
|
||||
root = append(root, rosa.Std.Load(rosa.Musl))
|
||||
}
|
||||
root = append(root,
|
||||
rosa.Std.Load(rosa.Mksh),
|
||||
rosa.Std.Load(rosa.Toybox),
|
||||
)
|
||||
|
||||
if _, _, err := cache.Cure(&root); err == nil {
|
||||
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
|
||||
@@ -573,11 +701,22 @@ func main() {
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
cured[a] = cureRes{pathname, checksum}
|
||||
}
|
||||
|
||||
layers := pkg.PromoteLayers(root, func(a pkg.Artifact) (
|
||||
@@ -587,7 +726,7 @@ func main() {
|
||||
res := cured[a]
|
||||
return res.pathname, res.checksum
|
||||
}, func(i int, d pkg.Artifact) {
|
||||
r := pkg.Encode(cache.Ident(d).Value())
|
||||
r := pkg.Encode(cm.c.Ident(d).Value())
|
||||
if s, ok := d.(fmt.Stringer); ok {
|
||||
if name := s.String(); name != "" {
|
||||
r += "-" + name
|
||||
@@ -606,6 +745,7 @@ func main() {
|
||||
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
|
||||
z.Quiet = !cm.verboseInit
|
||||
if s, ok := os.LookupEnv("TERM"); ok {
|
||||
z.Env = append(z.Env, "TERM="+s)
|
||||
}
|
||||
@@ -651,18 +791,15 @@ func main() {
|
||||
}
|
||||
return z.Wait()
|
||||
},
|
||||
).
|
||||
Flag(
|
||||
).Flag(
|
||||
&flagNet,
|
||||
"net", command.BoolFlag(false),
|
||||
"Share host net namespace",
|
||||
).
|
||||
Flag(
|
||||
).Flag(
|
||||
&flagSession,
|
||||
"session", command.BoolFlag(true),
|
||||
"Retain session",
|
||||
).
|
||||
Flag(
|
||||
).Flag(
|
||||
&flagWithToolchain,
|
||||
"with-toolchain", command.BoolFlag(false),
|
||||
"Include the stage2 LLVM toolchain",
|
||||
@@ -677,9 +814,7 @@ func main() {
|
||||
)
|
||||
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
if cache != nil {
|
||||
cache.Close()
|
||||
}
|
||||
cm.Close()
|
||||
if w, ok := err.(interface{ Unwrap() []error }); !ok {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
rosa.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 i := range rosa.PresetEnd {
|
||||
p := rosa.PArtifact(i)
|
||||
t.Run(rosa.GetMetadata(p).Name, func(t *testing.T) {
|
||||
_, err := cureRemote(t.Context(), &addr, rosa.Std.Load(p), 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
#endif
|
||||
|
||||
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
|
||||
#define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
|
||||
#define SHAREFS_PERM_REG 0600 /* permission bits for regular files presented to userspace */
|
||||
#define SHAREFS_PERM_DIR 0770 /* permission bits for directories presented to userspace */
|
||||
#define SHAREFS_PERM_REG 0660 /* permission bits for regular files presented to userspace */
|
||||
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
|
||||
|
||||
/* sharefs_private is populated by sharefs_init and contains process-wide context */
|
||||
|
||||
+10
-7
@@ -19,7 +19,6 @@ import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -470,13 +469,14 @@ func _main(s ...string) (exitCode int) {
|
||||
os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"),
|
||||
))
|
||||
|
||||
var setupWriter io.WriteCloser
|
||||
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
|
||||
var setupPipe [2]*os.File
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else {
|
||||
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
|
||||
setupWriter = w
|
||||
z.Args = append(z.Args, "-osetup="+strconv.Itoa(3+len(z.ExtraFiles)))
|
||||
z.ExtraFiles = append(z.ExtraFiles, r)
|
||||
setupPipe[0], setupPipe[1] = r, w
|
||||
}
|
||||
|
||||
if err := z.Start(); err != nil {
|
||||
@@ -487,6 +487,9 @@ func _main(s ...string) (exitCode int) {
|
||||
}
|
||||
return 5
|
||||
}
|
||||
if err := setupPipe[0].Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if err := z.Serve(); err != nil {
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
log.Println(m)
|
||||
@@ -496,10 +499,10 @@ func _main(s ...string) (exitCode int) {
|
||||
return 5
|
||||
}
|
||||
|
||||
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
|
||||
if err := gob.NewEncoder(setupPipe[1]).Encode(&setup); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else if err = setupWriter.Close(); err != nil {
|
||||
} else if err = setupPipe[1].Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -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 ./...'
|
||||
'')
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
//go:build raceattr
|
||||
|
||||
// The raceattr program reproduces vfs inode file attribute race.
|
||||
//
|
||||
// Even though libfuse high-level API presents the address of a struct stat
|
||||
// alongside struct fuse_context, file attributes are actually inherent to the
|
||||
// inode, instead of the specific call from userspace. The kernel implementation
|
||||
// in fs/fuse/xattr.c appears to make stale data in the inode (set by a previous
|
||||
// call) impossible or very unlikely to reach userspace via the stat family of
|
||||
// syscalls. However, when using default_permissions to have the VFS check
|
||||
// permissions, this race still happens, despite the resulting struct stat being
|
||||
// correct when overriding the check via capabilities otherwise.
|
||||
//
|
||||
// This program reproduces the failure, but because of its continuous nature, it
|
||||
// is provided independent of the vm integration test suite.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func newStatAs(
|
||||
ctx context.Context, cancel context.CancelFunc,
|
||||
n *atomic.Uint64, ok *atomic.Bool,
|
||||
uid uint32, pathname string,
|
||||
continuous bool,
|
||||
) func() {
|
||||
return func() {
|
||||
runtime.LockOSThread()
|
||||
defer cancel()
|
||||
|
||||
if _, _, errno := syscall.Syscall(
|
||||
syscall.SYS_SETUID, uintptr(uid),
|
||||
0, 0,
|
||||
); errno != 0 {
|
||||
cancel()
|
||||
log.Printf("cannot set uid to %d: %s", uid, errno)
|
||||
}
|
||||
|
||||
var stat syscall.Stat_t
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := syscall.Lstat(pathname, &stat); err != nil {
|
||||
// SHAREFS_PERM_DIR not world executable, or
|
||||
// SHAREFS_PERM_REG not world readable
|
||||
if !continuous {
|
||||
cancel()
|
||||
}
|
||||
ok.Store(true)
|
||||
log.Printf("uid %d: %v", uid, err)
|
||||
} else if stat.Uid != uid {
|
||||
// appears to be unreachable
|
||||
if !continuous {
|
||||
cancel()
|
||||
}
|
||||
ok.Store(true)
|
||||
log.Printf("got uid %d instead of %d", stat.Uid, uid)
|
||||
}
|
||||
n.Add(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("raceattr: ")
|
||||
|
||||
p := flag.String("target", "/sdcard/raceattr", "pathname of test file")
|
||||
u0 := flag.Int("uid0", 1<<10-1, "first uid")
|
||||
u1 := flag.Int("uid1", 1<<10-2, "second uid")
|
||||
count := flag.Int("count", 1, "threads per uid")
|
||||
continuous := flag.Bool("continuous", false, "keep running even after reproduce")
|
||||
flag.Parse()
|
||||
|
||||
if os.Geteuid() != 0 {
|
||||
log.Fatal("this program must run as root")
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(
|
||||
context.Background(),
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
syscall.SIGHUP,
|
||||
)
|
||||
|
||||
if err := os.WriteFile(*p, nil, 0); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
|
||||
n atomic.Uint64
|
||||
ok atomic.Bool
|
||||
)
|
||||
|
||||
if *count < 1 {
|
||||
*count = 1
|
||||
}
|
||||
for range *count {
|
||||
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u0), *p, *continuous))
|
||||
if *u1 >= 0 {
|
||||
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u1), *p, *continuous))
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
if !*continuous && ok.Load() {
|
||||
log.Printf("reproduced after %d calls", n.Load())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ const (
|
||||
CAP_SETPCAP = 0x8
|
||||
CAP_NET_ADMIN = 0xc
|
||||
CAP_DAC_OVERRIDE = 0x1
|
||||
CAP_SETFCAP = 0x1f
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
+72
-31
@@ -21,6 +21,7 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/internal/landlock"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -28,9 +29,6 @@ const (
|
||||
// CancelSignal is the signal expected by container init on context cancel.
|
||||
// A custom [Container.Cancel] function must eventually deliver this signal.
|
||||
CancelSignal = SIGUSR2
|
||||
|
||||
// Timeout for writing initParams to Container.setup.
|
||||
initSetupTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -53,7 +51,7 @@ type (
|
||||
ExtraFiles []*os.File
|
||||
|
||||
// Write end of a pipe connected to the init to deliver [Params].
|
||||
setup *os.File
|
||||
setup [2]*os.File
|
||||
// Cancels the context passed to the underlying cmd.
|
||||
cancel context.CancelFunc
|
||||
// Closed after Wait returns. Keeps the spawning thread alive.
|
||||
@@ -69,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
|
||||
@@ -90,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
|
||||
|
||||
@@ -215,6 +224,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
|
||||
@@ -285,16 +297,30 @@ 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 fd, f, err := Setup(&p.cmd.ExtraFiles); err != nil {
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "set up params stream",
|
||||
Err: err,
|
||||
}
|
||||
} else {
|
||||
p.setup = f
|
||||
fd := 3 + len(p.cmd.ExtraFiles)
|
||||
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, r)
|
||||
p.setup[0], p.setup[1] = r, w
|
||||
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
|
||||
}
|
||||
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
|
||||
@@ -308,7 +334,7 @@ func (p *Container) Start() error {
|
||||
done <- func() error {
|
||||
// PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes
|
||||
// created from the calling thread
|
||||
if err := SetNoNewPrivs(); err != nil {
|
||||
if err := setNoNewPrivs(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
|
||||
@@ -318,15 +344,17 @@ func (p *Container) Start() error {
|
||||
|
||||
// landlock: depends on per-thread state but acts on a process group
|
||||
{
|
||||
rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL}
|
||||
rulesetAttr := &landlock.RulesetAttr{
|
||||
Scoped: landlock.LANDLOCK_SCOPE_SIGNAL,
|
||||
}
|
||||
if !p.HostAbstract {
|
||||
rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
||||
rulesetAttr.Scoped |= landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
||||
}
|
||||
|
||||
if abi, err := LandlockGetABI(); err != nil {
|
||||
if p.HostAbstract {
|
||||
if abi, err := landlock.GetABI(); err != nil {
|
||||
if p.HostAbstract || !p.HostNet {
|
||||
// landlock can be skipped here as it restricts access
|
||||
// to resources already covered by namespaces (pid)
|
||||
// to resources already covered by namespaces (pid, net)
|
||||
goto landlockOut
|
||||
}
|
||||
return &StartError{Step: "get landlock ABI", Err: err}
|
||||
@@ -340,8 +368,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 {
|
||||
@@ -351,8 +377,7 @@ func (p *Container) Start() error {
|
||||
Err: err,
|
||||
}
|
||||
} else {
|
||||
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
|
||||
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
|
||||
if err = landlock.RestrictSelf(rulesetFd, 0); err != nil {
|
||||
_ = Close(rulesetFd)
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
@@ -408,7 +433,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",
|
||||
@@ -428,24 +452,33 @@ func (p *Container) Start() error {
|
||||
// Serve serves [Container.Params] to the container init.
|
||||
//
|
||||
// Serve must only be called once.
|
||||
func (p *Container) Serve() error {
|
||||
if p.setup == nil {
|
||||
func (p *Container) Serve() (err error) {
|
||||
if p.setup[0] == nil || p.setup[1] == nil {
|
||||
panic("invalid serve")
|
||||
}
|
||||
|
||||
setup := p.setup
|
||||
p.setup = nil
|
||||
if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil {
|
||||
done := make(chan struct{})
|
||||
defer func() {
|
||||
if closeErr := p.setup[1].Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
p.cancel()
|
||||
}
|
||||
close(done)
|
||||
p.setup[0], p.setup[1] = nil, nil
|
||||
}()
|
||||
if err = p.setup[0].Close(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "set init pipe deadline",
|
||||
Step: "close read end of init pipe",
|
||||
Err: err,
|
||||
Passthrough: true,
|
||||
}
|
||||
}
|
||||
|
||||
if p.Path == nil {
|
||||
p.cancel()
|
||||
return &StartError{
|
||||
Step: "invalid executable pathname",
|
||||
Err: EINVAL,
|
||||
@@ -461,18 +494,26 @@ func (p *Container) Serve() error {
|
||||
p.SeccompRules = make([]std.NativeRule, 0)
|
||||
}
|
||||
|
||||
err := gob.NewEncoder(setup).Encode(&initParams{
|
||||
t := time.Now().UTC()
|
||||
go func(f *os.File) {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
if cancelErr := f.SetWriteDeadline(t); cancelErr != nil {
|
||||
p.msg.Verbose(err)
|
||||
}
|
||||
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}(p.setup[1])
|
||||
|
||||
return gob.NewEncoder(p.setup[1]).Encode(&initParams{
|
||||
p.Params,
|
||||
Getuid(),
|
||||
Getgid(),
|
||||
len(p.ExtraFiles),
|
||||
p.msg.IsVerbose(),
|
||||
p.msg.IsVerbose() && !p.Quiet,
|
||||
})
|
||||
_ = setup.Close()
|
||||
if err != nil {
|
||||
p.cancel()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait blocks until the container init process to exit and releases any
|
||||
|
||||
+189
-55
@@ -16,6 +16,8 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/command"
|
||||
@@ -25,6 +27,9 @@ import (
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/landlock"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/ldd"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/vfs"
|
||||
@@ -83,9 +88,9 @@ func TestStartError(t *testing.T) {
|
||||
{"params env", &container.StartError{
|
||||
Fatal: true,
|
||||
Step: "set up params stream",
|
||||
Err: container.ErrReceiveEnv,
|
||||
Err: params.ErrReceiveEnv,
|
||||
}, "set up params stream: environment variable not set",
|
||||
container.ErrReceiveEnv, syscall.EBADF,
|
||||
params.ErrReceiveEnv, syscall.EBADF,
|
||||
"cannot set up params stream: environment variable not set"},
|
||||
|
||||
{"params", &container.StartError{
|
||||
@@ -230,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
|
||||
@@ -329,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"),
|
||||
}
|
||||
},
|
||||
@@ -385,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"),
|
||||
}
|
||||
},
|
||||
@@ -397,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)
|
||||
@@ -453,6 +437,20 @@ 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) {
|
||||
t.Fatalf("LandlockGetABI: error = %v", err)
|
||||
}
|
||||
c.HostAbstract = true
|
||||
t.Log("Landlock LSM is unavailable, enabling HostAbstract")
|
||||
}
|
||||
}
|
||||
if c.InitAsRoot {
|
||||
c.SeccompPresets &= ^std.PresetDenyNS
|
||||
}
|
||||
|
||||
c.
|
||||
Readonly(check.MustAbs(pathReadonly), 0755).
|
||||
@@ -521,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 {
|
||||
@@ -543,11 +546,10 @@ 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()
|
||||
waitCheck func(ps *os.ProcessState, waitErr error),
|
||||
) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
c := helperNewContainer(ctx, "block")
|
||||
@@ -557,25 +559,36 @@ func testContainerCancel(
|
||||
}
|
||||
|
||||
ready := make(chan struct{})
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
var waitErr error
|
||||
r, w, err := os.Pipe()
|
||||
if 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 := r.Read(make([]byte, 1)); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if err := c.Start(); err != nil {
|
||||
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 {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -584,8 +597,67 @@ func testContainerCancel(
|
||||
}
|
||||
<-ready
|
||||
cancel()
|
||||
waitCheck(t, c)
|
||||
<-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) {
|
||||
@@ -621,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
|
||||
}
|
||||
@@ -638,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)
|
||||
@@ -755,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
|
||||
|
||||
+10
-4
@@ -16,6 +16,7 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/internal/netlink"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -56,7 +57,7 @@ type syscallDispatcher interface {
|
||||
// isatty provides [Isatty].
|
||||
isatty(fd int) bool
|
||||
// receive provides [Receive].
|
||||
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
||||
receive(key string, e any, fdp *int) (closeFunc func() error, err error)
|
||||
|
||||
// bindMount provides procPaths.bindMount.
|
||||
bindMount(msg message.Msg, source, target string, flags uintptr) error
|
||||
@@ -64,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.
|
||||
@@ -147,7 +150,7 @@ func (direct) lockOSThread() { runtime.LockOSThread() }
|
||||
|
||||
func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) }
|
||||
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
|
||||
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
|
||||
func (direct) setNoNewPrivs() error { return setNoNewPrivs() }
|
||||
|
||||
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
|
||||
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
|
||||
@@ -155,8 +158,8 @@ func (direct) capBoundingSetDrop(cap uintptr) error { return capBound
|
||||
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
|
||||
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
|
||||
func (direct) isatty(fd int) bool { return ext.Isatty(fd) }
|
||||
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
return Receive(key, e, fdp)
|
||||
func (direct) receive(key string, e any, fdp *int) (func() error, error) {
|
||||
return params.Receive(key, e, fdp)
|
||||
}
|
||||
|
||||
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
|
||||
@@ -168,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)
|
||||
}
|
||||
|
||||
@@ -390,7 +390,7 @@ func (k *kstub) isatty(fd int) bool {
|
||||
return expect.Ret.(bool)
|
||||
}
|
||||
|
||||
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
||||
func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, err error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("receive")
|
||||
|
||||
@@ -408,10 +408,17 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// avoid changing test cases
|
||||
var fdpComp *uintptr
|
||||
if fdp != nil {
|
||||
fdpComp = new(uintptr(*fdp))
|
||||
}
|
||||
|
||||
err = expect.Error(
|
||||
stub.CheckArg(k.Stub, "key", key, 0),
|
||||
stub.CheckArgReflect(k.Stub, "e", e, 1),
|
||||
stub.CheckArgReflect(k.Stub, "fdp", fdp, 2))
|
||||
stub.CheckArgReflect(k.Stub, "fdp", fdpComp, 2))
|
||||
|
||||
// 3 is unused so stores params
|
||||
if expect.Args[3] != nil {
|
||||
@@ -426,7 +433,7 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
|
||||
if expect.Args[4] != nil {
|
||||
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
|
||||
if fdp != nil {
|
||||
*fdp = v
|
||||
*fdp = int(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,6 +468,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(
|
||||
|
||||
@@ -118,6 +118,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
|
||||
|
||||
+138
-56
@@ -11,14 +11,17 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
. "syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -147,35 +150,33 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
var (
|
||||
params initParams
|
||||
param initParams
|
||||
closeSetup func() error
|
||||
setupFd uintptr
|
||||
offsetSetup int
|
||||
setupFd int
|
||||
)
|
||||
if f, err := k.receive(setupEnv, ¶ms, &setupFd); err != nil {
|
||||
if f, err := k.receive(setupEnv, ¶m, &setupFd); err != nil {
|
||||
if errors.Is(err, EBADF) {
|
||||
k.fatal(msg, "invalid setup descriptor")
|
||||
}
|
||||
if errors.Is(err, ErrReceiveEnv) {
|
||||
if errors.Is(err, params.ErrReceiveEnv) {
|
||||
k.fatal(msg, setupEnv+" not set")
|
||||
}
|
||||
|
||||
k.fatalf(msg, "cannot decode init setup payload: %v", err)
|
||||
} else {
|
||||
if params.Ops == nil {
|
||||
if param.Ops == nil {
|
||||
k.fatal(msg, "invalid setup parameters")
|
||||
}
|
||||
if params.ParentPerm == 0 {
|
||||
params.ParentPerm = 0755
|
||||
if param.ParentPerm == 0 {
|
||||
param.ParentPerm = 0755
|
||||
}
|
||||
|
||||
msg.SwapVerbose(params.Verbose)
|
||||
msg.SwapVerbose(param.Verbose)
|
||||
msg.Verbose("received setup parameters")
|
||||
closeSetup = f
|
||||
offsetSetup = int(setupFd + 1)
|
||||
}
|
||||
|
||||
if !params.HostNet {
|
||||
if !param.HostNet {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), CancelSignal,
|
||||
os.Interrupt, SIGTERM, SIGQUIT)
|
||||
defer cancel() // for panics
|
||||
@@ -183,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(params.Uid)+" "+strconv.Itoa(params.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(params.Gid)+" "+strconv.Itoa(params.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 {
|
||||
@@ -207,8 +218,8 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
oldmask := k.umask(0)
|
||||
if params.Hostname != "" {
|
||||
if err := k.sethostname([]byte(params.Hostname)); err != nil {
|
||||
if param.Hostname != "" {
|
||||
if err := k.sethostname([]byte(param.Hostname)); err != nil {
|
||||
k.fatalf(msg, "cannot set hostname: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -221,15 +232,32 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
state := &setupState{process: make(map[int]WaitStatus), Params: ¶ms.Params, Msg: msg, Context: ctx}
|
||||
state := &setupState{process: make(map[int]WaitStatus), Params: ¶m.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
|
||||
implementations are expected to avoid changing the state of the mount
|
||||
namespace */
|
||||
for i, op := range *params.Ops {
|
||||
for i, op := range *param.Ops {
|
||||
if op == nil || !op.Valid() {
|
||||
k.fatalf(msg, "invalid op at index %d", i)
|
||||
}
|
||||
@@ -243,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)
|
||||
}
|
||||
@@ -272,7 +293,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
step sets up the container filesystem, and implementations are expected to
|
||||
keep the host root and sysroot mount points intact but otherwise can do
|
||||
whatever they need to. Calling chdir is allowed but discouraged. */
|
||||
for i, op := range *params.Ops {
|
||||
for i, op := range *param.Ops {
|
||||
// ops already checked during early setup
|
||||
if prefix, ok := op.prefix(); ok {
|
||||
msg.Verbosef("%s %s", prefix, op)
|
||||
@@ -286,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))
|
||||
@@ -324,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 params.Privileged && i == CAP_SYS_ADMIN {
|
||||
for i := range lastcap + 1 {
|
||||
if slices.Contains(keepCaps, i) {
|
||||
continue
|
||||
}
|
||||
if err := k.capBoundingSetDrop(i); err != nil {
|
||||
@@ -337,27 +408,30 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
var keep [2]uint32
|
||||
if params.Privileged {
|
||||
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
|
||||
for _, c := range keepCaps {
|
||||
keep[capToIndex(c)] |= capToMask(c)
|
||||
}
|
||||
|
||||
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
|
||||
k.fatalf(msg, "cannot raise CAP_SYS_ADMIN: %v", err)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
if !params.SeccompDisable {
|
||||
rules := params.SeccompRules
|
||||
if len(rules) == 0 { // non-empty rules slice always overrides presets
|
||||
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
|
||||
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
|
||||
for _, c := range keepCaps {
|
||||
if err := k.capAmbientRaise(c); err != nil {
|
||||
k.fatalf(msg, "cannot raise %#x: %v", c, err)
|
||||
}
|
||||
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
|
||||
}
|
||||
|
||||
if !param.SeccompDisable {
|
||||
rules := param.SeccompRules
|
||||
if len(rules) == 0 { // non-empty rules slice always overrides presets
|
||||
msg.Verbosef("resolving presets %#x", param.SeccompPresets)
|
||||
rules = seccomp.Preset(param.SeccompPresets, param.SeccompFlags)
|
||||
}
|
||||
if err := k.seccompLoad(rules, param.SeccompFlags); err != nil {
|
||||
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
|
||||
k.fatalf(msg, "cannot load syscall filter: %v", err)
|
||||
}
|
||||
@@ -366,10 +440,10 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
msg.Verbose("syscall filter not configured")
|
||||
}
|
||||
|
||||
extraFiles := make([]*os.File, params.Count)
|
||||
extraFiles := make([]*os.File, param.Count)
|
||||
for i := range extraFiles {
|
||||
// setup fd is placed before all extra files
|
||||
extraFiles[i] = k.newFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
|
||||
extraFiles[i] = k.newFile(uintptr(setupFd+1+i), "extra file "+strconv.Itoa(i))
|
||||
}
|
||||
k.umask(oldmask)
|
||||
|
||||
@@ -447,7 +521,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
|
||||
// called right before startup of initial process, all state changes to the
|
||||
// current process is prohibited during late
|
||||
for i, op := range *params.Ops {
|
||||
for i, op := range *param.Ops {
|
||||
// ops already checked during early setup
|
||||
if err := op.late(state, k); err != nil {
|
||||
if m, ok := messageFromError(err); ok {
|
||||
@@ -468,14 +542,22 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "cannot close setup pipe: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(params.Path.String())
|
||||
cmd := exec.Command(param.Path.String())
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.Args = params.Args
|
||||
cmd.Env = params.Env
|
||||
cmd.Args = param.Args
|
||||
cmd.Env = param.Env
|
||||
cmd.ExtraFiles = extraFiles
|
||||
cmd.Dir = params.Dir.String()
|
||||
cmd.Dir = param.Dir.String()
|
||||
|
||||
msg.Verbosef("starting initial process %s", params.Path)
|
||||
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)
|
||||
}
|
||||
@@ -493,9 +575,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
for {
|
||||
select {
|
||||
case s := <-sig:
|
||||
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
|
||||
if s == CancelSignal && param.ForwardCancel && cmd.Process != nil {
|
||||
msg.Verbose("forwarding context cancellation")
|
||||
if err := k.signal(cmd, os.Interrupt); err != nil {
|
||||
if err := k.signal(cmd, os.Interrupt); err != nil && !errors.Is(err, os.ErrProcessDone) {
|
||||
k.printf(msg, "cannot forward cancellation: %v", err)
|
||||
}
|
||||
continue
|
||||
@@ -525,7 +607,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
cancel()
|
||||
|
||||
// start timeout early
|
||||
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
|
||||
go func() { time.Sleep(param.AdoptWaitDelay); close(timeout) }()
|
||||
|
||||
// close initial process files; this also keeps them alive
|
||||
for _, f := range extraFiles {
|
||||
|
||||
+75
-74
@@ -10,6 +10,7 @@ import (
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/internal/stub"
|
||||
)
|
||||
|
||||
@@ -40,7 +41,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("getpid", stub.ExpectArgs{}, 1, nil),
|
||||
call("setPtracer", stub.ExpectArgs{uintptr(0)}, nil, nil),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, ErrReceiveEnv),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, params.ErrReceiveEnv),
|
||||
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SETUP not set"}}, nil, nil),
|
||||
},
|
||||
}, nil},
|
||||
@@ -331,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 */
|
||||
@@ -369,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 */
|
||||
@@ -407,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),
|
||||
@@ -446,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),
|
||||
@@ -485,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),
|
||||
},
|
||||
@@ -525,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),
|
||||
@@ -566,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),
|
||||
},
|
||||
@@ -608,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),
|
||||
@@ -651,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)),
|
||||
@@ -695,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),
|
||||
@@ -740,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),
|
||||
@@ -786,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),
|
||||
@@ -841,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),
|
||||
@@ -896,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),
|
||||
@@ -952,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),
|
||||
@@ -1009,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),
|
||||
@@ -1068,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),
|
||||
@@ -1128,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),
|
||||
@@ -1189,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),
|
||||
@@ -1251,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),
|
||||
@@ -1314,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),
|
||||
@@ -1378,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),
|
||||
@@ -1443,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),
|
||||
@@ -1509,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),
|
||||
@@ -1583,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),
|
||||
@@ -1621,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),
|
||||
@@ -1653,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},
|
||||
|
||||
@@ -1690,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),
|
||||
@@ -1728,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),
|
||||
@@ -1760,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},
|
||||
@@ -1798,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),
|
||||
@@ -1836,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),
|
||||
@@ -1868,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),
|
||||
@@ -1907,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),
|
||||
@@ -2031,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),
|
||||
@@ -2131,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),
|
||||
@@ -2231,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),
|
||||
@@ -2322,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),
|
||||
@@ -2417,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),
|
||||
@@ -2519,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),
|
||||
@@ -2658,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),
|
||||
@@ -2696,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),
|
||||
@@ -2728,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
@@ -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 }
|
||||
|
||||
@@ -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},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/container"
|
||||
)
|
||||
|
||||
func TestLandlockString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
rulesetAttr *container.RulesetAttr
|
||||
want string
|
||||
}{
|
||||
{"nil", nil, "NULL"},
|
||||
{"zero", new(container.RulesetAttr), "0"},
|
||||
{"some", &container.RulesetAttr{Scoped: container.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
|
||||
{"set", &container.RulesetAttr{
|
||||
HandledAccessFS: container.LANDLOCK_ACCESS_FS_MAKE_SYM | container.LANDLOCK_ACCESS_FS_IOCTL_DEV | container.LANDLOCK_ACCESS_FS_WRITE_FILE,
|
||||
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP,
|
||||
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | container.LANDLOCK_SCOPE_SIGNAL,
|
||||
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
|
||||
{"all", &container.RulesetAttr{
|
||||
HandledAccessFS: container.LANDLOCK_ACCESS_FS_EXECUTE |
|
||||
container.LANDLOCK_ACCESS_FS_WRITE_FILE |
|
||||
container.LANDLOCK_ACCESS_FS_READ_FILE |
|
||||
container.LANDLOCK_ACCESS_FS_READ_DIR |
|
||||
container.LANDLOCK_ACCESS_FS_REMOVE_DIR |
|
||||
container.LANDLOCK_ACCESS_FS_REMOVE_FILE |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_CHAR |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_DIR |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_REG |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_SOCK |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_FIFO |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_SYM |
|
||||
container.LANDLOCK_ACCESS_FS_REFER |
|
||||
container.LANDLOCK_ACCESS_FS_TRUNCATE |
|
||||
container.LANDLOCK_ACCESS_FS_IOCTL_DEV,
|
||||
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP |
|
||||
container.LANDLOCK_ACCESS_NET_CONNECT_TCP,
|
||||
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
|
||||
container.LANDLOCK_SCOPE_SIGNAL,
|
||||
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := tc.rulesetAttr.String(); got != tc.want {
|
||||
t.Errorf("String: %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLandlockAttrSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := 24
|
||||
if got := unsafe.Sizeof(container.RulesetAttr{}); got != uintptr(want) {
|
||||
t.Errorf("Sizeof: %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
|
||||
func Setup(extraFiles *[]*os.File) (int, *os.File, error) {
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
return -1, nil, err
|
||||
} else {
|
||||
fd := 3 + len(*extraFiles)
|
||||
*extraFiles = append(*extraFiles, r)
|
||||
return fd, w, nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrReceiveEnv = errors.New("environment variable not set")
|
||||
)
|
||||
|
||||
// Receive retrieves setup fd from the environment and receives params.
|
||||
func Receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
var setup *os.File
|
||||
|
||||
if s, ok := os.LookupEnv(key); !ok {
|
||||
return nil, ErrReceiveEnv
|
||||
} else {
|
||||
if fd, err := strconv.Atoi(s); err != nil {
|
||||
return nil, optionalErrorUnwrap(err)
|
||||
} else {
|
||||
setup = os.NewFile(uintptr(fd), "setup")
|
||||
if setup == nil {
|
||||
return nil, syscall.EDOM
|
||||
}
|
||||
if fdp != nil {
|
||||
*fdp = setup.Fd()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return setup.Close, gob.NewDecoder(setup).Decode(e)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"hakurei.app/ext"
|
||||
)
|
||||
|
||||
// SetNoNewPrivs sets the calling thread's no_new_privs attribute.
|
||||
func SetNoNewPrivs() error {
|
||||
// setNoNewPrivs sets the calling thread's no_new_privs attribute.
|
||||
func setNoNewPrivs() error {
|
||||
return ext.Prctl(PR_SET_NO_NEW_PRIVS, 1, 0)
|
||||
}
|
||||
|
||||
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
1000 0
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname -- "$0")" || exit 1
|
||||
|
||||
install -vDm0755 "bin/hakurei" "${DESTDIR}/usr/bin/hakurei"
|
||||
install -vDm0755 "bin/sharefs" "${DESTDIR}/usr/bin/sharefs"
|
||||
|
||||
install -vDm4511 "bin/hsu" "${DESTDIR}/usr/bin/hsu"
|
||||
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
|
||||
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
|
||||
fi
|
||||
|
||||
install -vDm0644 "comp/_hakurei" "${DESTDIR}/usr/share/zsh/site-functions/_hakurei"
|
||||
Vendored
-31
@@ -1,31 +0,0 @@
|
||||
#!/bin/sh -e
|
||||
cd "$(dirname -- "$0")/.."
|
||||
VERSION="${HAKUREI_VERSION:-untagged}"
|
||||
pname="hakurei-${VERSION}-$(go env GOARCH)"
|
||||
out="${DESTDIR:-dist}/${pname}"
|
||||
|
||||
echo '# Preparing distribution files.'
|
||||
mkdir -p "${out}"
|
||||
cp -v "README.md" "dist/hsurc.default" "dist/install.sh" "${out}"
|
||||
cp -rv "dist/comp" "${out}"
|
||||
echo
|
||||
|
||||
echo '# Building hakurei.'
|
||||
go generate ./...
|
||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w
|
||||
-buildid= -linkmode external -extldflags=-static
|
||||
-X hakurei.app/internal/info.buildVersion=${VERSION}
|
||||
-X hakurei.app/internal/info.hakureiPath=/usr/bin/hakurei
|
||||
-X hakurei.app/internal/info.hsuPath=/usr/bin/hsu
|
||||
-X main.hakureiPath=/usr/bin/hakurei" ./...
|
||||
echo
|
||||
|
||||
echo '# Testing hakurei.'
|
||||
go test -ldflags='-buildid= -linkmode external -extldflags=-static' ./...
|
||||
echo
|
||||
|
||||
echo '# Creating distribution.'
|
||||
rm -f "${out}.tar.gz" && tar -C "${out}/.." -vczf "${out}.tar.gz" "${pname}"
|
||||
rm -rf "${out}"
|
||||
(cd "${out}/.." && sha512sum "${pname}.tar.gz" > "${pname}.tar.gz.sha512")
|
||||
echo
|
||||
@@ -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
|
||||
}
|
||||
@@ -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].
|
||||
|
||||
@@ -137,11 +137,10 @@
|
||||
|
||||
CC="musl-clang -O3 -Werror -Qunused-arguments" \
|
||||
GOCACHE="$(mktemp -d)" \
|
||||
HAKUREI_TEST_SKIP_ACL=1 \
|
||||
PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \
|
||||
DESTDIR="$out" \
|
||||
HAKUREI_VERSION="v${hakurei.version}" \
|
||||
./dist/release.sh
|
||||
./all.sh
|
||||
'';
|
||||
}
|
||||
);
|
||||
@@ -196,6 +195,7 @@
|
||||
./test/interactive/vm.nix
|
||||
./test/interactive/hakurei.nix
|
||||
./test/interactive/trace.nix
|
||||
./test/interactive/raceattr.nix
|
||||
|
||||
self.nixosModules.hakurei
|
||||
home-manager.nixosModules.home-manager
|
||||
|
||||
+36
-11
@@ -140,21 +140,29 @@ var (
|
||||
ErrInsecure = errors.New("configuration is insecure")
|
||||
)
|
||||
|
||||
const (
|
||||
// VAllowInsecure allows use of compatibility options considered insecure
|
||||
// under any configuration, to work around ecosystem-wide flaws.
|
||||
VAllowInsecure = 1 << iota
|
||||
)
|
||||
|
||||
// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
|
||||
func (config *Config) Validate() error {
|
||||
func (config *Config) Validate(flags int) error {
|
||||
const step = "validate configuration"
|
||||
|
||||
if config == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "invalid configuration"}
|
||||
}
|
||||
|
||||
// this is checked again in hsu
|
||||
if config.Identity < IdentityStart || config.Identity > IdentityEnd {
|
||||
return &AppError{Step: "validate configuration", Err: ErrIdentityBounds,
|
||||
return &AppError{Step: step, Err: ErrIdentityBounds,
|
||||
Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
|
||||
}
|
||||
|
||||
if config.SchedPolicy < 0 || config.SchedPolicy > ext.SCHED_LAST {
|
||||
return &AppError{Step: "validate configuration", Err: ErrSchedPolicyBounds,
|
||||
return &AppError{Step: step, Err: ErrSchedPolicyBounds,
|
||||
Msg: "scheduling policy " +
|
||||
strconv.Itoa(int(config.SchedPolicy)) +
|
||||
" out of range"}
|
||||
@@ -168,34 +176,51 @@ func (config *Config) Validate() error {
|
||||
}
|
||||
|
||||
if config.Container == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "configuration missing container state"}
|
||||
}
|
||||
if config.Container.Home == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "container configuration missing path to home directory"}
|
||||
}
|
||||
if config.Container.Shell == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "container configuration missing path to shell"}
|
||||
}
|
||||
if config.Container.Path == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "container configuration missing path to initial program"}
|
||||
}
|
||||
|
||||
for key := range config.Container.Env {
|
||||
if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 {
|
||||
return &AppError{Step: "validate configuration", Err: ErrEnviron,
|
||||
return &AppError{Step: step, Err: ErrEnviron,
|
||||
Msg: "invalid environment variable " + strconv.Quote(key)}
|
||||
}
|
||||
}
|
||||
|
||||
if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
|
||||
return &AppError{Step: "validate configuration", Err: ErrInsecure,
|
||||
et := config.Enablements.Unwrap()
|
||||
if !config.DirectPulse && et&EPulse != 0 {
|
||||
return &AppError{Step: step, Err: ErrInsecure,
|
||||
Msg: "enablement PulseAudio is insecure and no longer supported"}
|
||||
}
|
||||
|
||||
if flags&VAllowInsecure == 0 {
|
||||
switch {
|
||||
case et&EWayland != 0 && config.DirectWayland:
|
||||
return &AppError{Step: step, Err: ErrInsecure,
|
||||
Msg: "direct_wayland is insecure and no longer supported"}
|
||||
|
||||
case et&EPipeWire != 0 && config.DirectPipeWire:
|
||||
return &AppError{Step: step, Err: ErrInsecure,
|
||||
Msg: "direct_pipewire is insecure and no longer supported"}
|
||||
|
||||
case et&EPulse != 0 && config.DirectPulse:
|
||||
return &AppError{Step: step, Err: ErrInsecure,
|
||||
Msg: "direct_pulse is insecure and no longer supported"}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+61
-17
@@ -14,65 +14,109 @@ func TestConfigValidate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config *hst.Config
|
||||
flags int
|
||||
wantErr error
|
||||
}{
|
||||
{"nil", nil, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
{"nil", nil, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "invalid configuration"}},
|
||||
{"identity lower", &hst.Config{Identity: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
||||
|
||||
{"identity lower", &hst.Config{Identity: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
||||
Msg: "identity -1 out of range"}},
|
||||
{"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
||||
{"identity upper", &hst.Config{Identity: 10000}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
||||
Msg: "identity 10000 out of range"}},
|
||||
{"sched lower", &hst.Config{SchedPolicy: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
||||
|
||||
{"sched lower", &hst.Config{SchedPolicy: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
||||
Msg: "scheduling policy -1 out of range"}},
|
||||
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
||||
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
||||
Msg: "scheduling policy 51966 out of range"}},
|
||||
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}},
|
||||
|
||||
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}}, 0,
|
||||
&hst.BadInterfaceError{Interface: "", Segment: "session"}},
|
||||
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}},
|
||||
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}}, 0,
|
||||
&hst.BadInterfaceError{Interface: "", Segment: "system"}},
|
||||
{"container", &hst.Config{}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
|
||||
{"container", &hst.Config{}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "configuration missing container state"}},
|
||||
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "container configuration missing path to home directory"}},
|
||||
{"shell", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "container configuration missing path to shell"}},
|
||||
{"path", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "container configuration missing path to initial program"}},
|
||||
|
||||
{"env equals", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
Env: map[string]string{"TERM=": ""},
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
Msg: `invalid environment variable "TERM="`}},
|
||||
{"env NUL", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
Env: map[string]string{"TERM\x00": ""},
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
Msg: `invalid environment variable "TERM\x00"`}},
|
||||
{"insecure pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), Container: &hst.ContainerConfig{
|
||||
|
||||
{"insecure pulse", &hst.Config{Enablements: new(hst.EPulse), Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
Msg: "enablement PulseAudio is insecure and no longer supported"}},
|
||||
|
||||
{"direct wayland", &hst.Config{Enablements: new(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
Msg: "direct_wayland is insecure and no longer supported"}},
|
||||
{"direct wayland allow", &hst.Config{Enablements: new(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, hst.VAllowInsecure, nil},
|
||||
|
||||
{"direct pipewire", &hst.Config{Enablements: new(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
Msg: "direct_pipewire is insecure and no longer supported"}},
|
||||
{"direct pipewire allow", &hst.Config{Enablements: new(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, hst.VAllowInsecure, nil},
|
||||
|
||||
{"direct pulse", &hst.Config{Enablements: new(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
Msg: "direct_pulse is insecure and no longer supported"}},
|
||||
{"direct pulse allow", &hst.Config{Enablements: new(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, hst.VAllowInsecure, nil},
|
||||
|
||||
{"valid", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, nil},
|
||||
}}, 0, nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := tc.config.Validate(); !reflect.DeepEqual(err, tc.wantErr) {
|
||||
if err := tc.config.Validate(tc.flags); !reflect.DeepEqual(err, tc.wantErr) {
|
||||
t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
+21
-31
@@ -7,12 +7,12 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Enablement represents an optional host service to export to the target user.
|
||||
type Enablement byte
|
||||
// Enablements denotes optional host service to export to the target user.
|
||||
type Enablements byte
|
||||
|
||||
const (
|
||||
// EWayland exposes a Wayland pathname socket via security-context-v1.
|
||||
EWayland Enablement = 1 << iota
|
||||
EWayland Enablements = 1 << iota
|
||||
// EX11 adds the target user via X11 ChangeHosts and exposes the X11
|
||||
// pathname socket.
|
||||
EX11
|
||||
@@ -28,8 +28,8 @@ const (
|
||||
EM
|
||||
)
|
||||
|
||||
// String returns a string representation of the flags set on [Enablement].
|
||||
func (e Enablement) String() string {
|
||||
// String returns a string representation of the flags set on [Enablements].
|
||||
func (e Enablements) String() string {
|
||||
switch e {
|
||||
case 0:
|
||||
return "(no enablements)"
|
||||
@@ -47,7 +47,7 @@ func (e Enablement) String() string {
|
||||
buf := new(strings.Builder)
|
||||
buf.Grow(32)
|
||||
|
||||
for i := Enablement(1); i < EM; i <<= 1 {
|
||||
for i := Enablements(1); i < EM; i <<= 1 {
|
||||
if e&i != 0 {
|
||||
buf.WriteString(", " + i.String())
|
||||
}
|
||||
@@ -60,12 +60,6 @@ func (e Enablement) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// NewEnablements returns the address of [Enablement] as [Enablements].
|
||||
func NewEnablements(e Enablement) *Enablements { return (*Enablements)(&e) }
|
||||
|
||||
// Enablements is the [json] adapter for [Enablement].
|
||||
type Enablements Enablement
|
||||
|
||||
// enablementsJSON is the [json] representation of [Enablements].
|
||||
type enablementsJSON = struct {
|
||||
Wayland bool `json:"wayland,omitempty"`
|
||||
@@ -75,24 +69,21 @@ type enablementsJSON = struct {
|
||||
Pulse bool `json:"pulse,omitempty"`
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying [Enablement].
|
||||
func (e *Enablements) Unwrap() Enablement {
|
||||
// Unwrap returns the value pointed to by e.
|
||||
func (e *Enablements) Unwrap() Enablements {
|
||||
if e == nil {
|
||||
return 0
|
||||
}
|
||||
return Enablement(*e)
|
||||
return *e
|
||||
}
|
||||
|
||||
func (e *Enablements) MarshalJSON() ([]byte, error) {
|
||||
if e == nil {
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
func (e Enablements) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&enablementsJSON{
|
||||
Wayland: Enablement(*e)&EWayland != 0,
|
||||
X11: Enablement(*e)&EX11 != 0,
|
||||
DBus: Enablement(*e)&EDBus != 0,
|
||||
PipeWire: Enablement(*e)&EPipeWire != 0,
|
||||
Pulse: Enablement(*e)&EPulse != 0,
|
||||
Wayland: e&EWayland != 0,
|
||||
X11: e&EX11 != 0,
|
||||
DBus: e&EDBus != 0,
|
||||
PipeWire: e&EPipeWire != 0,
|
||||
Pulse: e&EPulse != 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,22 +97,21 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var ve Enablement
|
||||
*e = 0
|
||||
if v.Wayland {
|
||||
ve |= EWayland
|
||||
*e |= EWayland
|
||||
}
|
||||
if v.X11 {
|
||||
ve |= EX11
|
||||
*e |= EX11
|
||||
}
|
||||
if v.DBus {
|
||||
ve |= EDBus
|
||||
*e |= EDBus
|
||||
}
|
||||
if v.PipeWire {
|
||||
ve |= EPipeWire
|
||||
*e |= EPipeWire
|
||||
}
|
||||
if v.Pulse {
|
||||
ve |= EPulse
|
||||
*e |= EPulse
|
||||
}
|
||||
*e = Enablements(ve)
|
||||
return nil
|
||||
}
|
||||
|
||||
+9
-12
@@ -13,7 +13,7 @@ func TestEnablementString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
flags hst.Enablement
|
||||
flags hst.Enablements
|
||||
want string
|
||||
}{
|
||||
{0, "(no enablements)"},
|
||||
@@ -59,13 +59,13 @@ func TestEnablements(t *testing.T) {
|
||||
sData string
|
||||
}{
|
||||
{"nil", nil, "null", `{"value":null,"magic":3236757504}`},
|
||||
{"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`},
|
||||
{"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
|
||||
{"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
|
||||
{"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
|
||||
{"pipewire", hst.NewEnablements(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
|
||||
{"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
|
||||
{"all", hst.NewEnablements(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`},
|
||||
{"zero", new(hst.Enablements(0)), `{}`, `{"value":{},"magic":3236757504}`},
|
||||
{"wayland", new(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
|
||||
{"x11", new(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
|
||||
{"dbus", new(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
|
||||
{"pipewire", new(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
|
||||
{"pulse", new(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
|
||||
{"all", new(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -137,7 +137,7 @@ func TestEnablements(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("val", func(t *testing.T) {
|
||||
if got := hst.NewEnablements(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
|
||||
if got := new(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
|
||||
t.Errorf("Unwrap: %v", got)
|
||||
}
|
||||
})
|
||||
@@ -146,9 +146,6 @@ func TestEnablements(t *testing.T) {
|
||||
t.Run("passthrough", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := (*hst.Enablements)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) {
|
||||
t.Errorf("MarshalJSON: error = %v", err)
|
||||
}
|
||||
if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) {
|
||||
t.Errorf("UnmarshalJSON: error = %v", err)
|
||||
}
|
||||
|
||||
@@ -56,8 +56,10 @@ type Ops interface {
|
||||
|
||||
// ApplyState holds the address of [Ops] and any relevant application state.
|
||||
type ApplyState struct {
|
||||
// AutoEtcPrefix is the prefix for [FSBind] in autoetc [FSBind.Special] condition.
|
||||
// Prefix for [FSBind] in autoetc [FSBind.Special] condition.
|
||||
AutoEtcPrefix string
|
||||
// Whether to skip remounting root.
|
||||
NoRemountRoot bool
|
||||
|
||||
Ops
|
||||
}
|
||||
|
||||
+6
-2
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/fhs"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(FSOverlay)) }
|
||||
@@ -69,9 +70,12 @@ func (o *FSOverlay) Apply(z *ApplyState) {
|
||||
return
|
||||
}
|
||||
|
||||
if o.Upper != nil && o.Work != nil { // rw
|
||||
if o.Upper != nil && o.Work != nil {
|
||||
z.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
|
||||
} else { // ro
|
||||
if o.Target.Is(fhs.AbsRoot) {
|
||||
z.NoRemountRoot = true
|
||||
}
|
||||
} else {
|
||||
z.OverlayReadonly(o.Target, o.Lower...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,5 +49,18 @@ func TestFSOverlay(t *testing.T) {
|
||||
Lower: ms("/tmp/.src0", "/tmp/.src1"),
|
||||
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
|
||||
"*/mnt/src:/tmp/.src0:/tmp/.src1"},
|
||||
|
||||
{"no remount root", &hst.FSOverlay{
|
||||
Target: m("/"),
|
||||
Lower: ms("/tmp/.src0", "/tmp/.src1"),
|
||||
Upper: m("/tmp/upper"),
|
||||
Work: m("/tmp/work"),
|
||||
}, true, container.Ops{&container.MountOverlayOp{
|
||||
Target: m("/"),
|
||||
Lower: ms("/tmp/.src0", "/tmp/.src1"),
|
||||
Upper: m("/tmp/upper"),
|
||||
Work: m("/tmp/work"),
|
||||
}}, m("/"), ms("/tmp/upper", "/tmp/work", "/tmp/.src0", "/tmp/.src1"),
|
||||
"w*/:/tmp/upper:/tmp/work:/tmp/.src0:/tmp/.src1"},
|
||||
})
|
||||
}
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ func Template() *Config {
|
||||
return &Config{
|
||||
ID: "org.chromium.Chromium",
|
||||
|
||||
Enablements: NewEnablements(EWayland | EDBus | EPipeWire),
|
||||
Enablements: new(EWayland | EDBus | EPipeWire),
|
||||
|
||||
SessionBus: &BusConfig{
|
||||
See: nil,
|
||||
|
||||
@@ -11,9 +11,11 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/info"
|
||||
)
|
||||
|
||||
const testFileName = "acl.test"
|
||||
@@ -24,8 +26,14 @@ var (
|
||||
)
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
if os.Getenv("HAKUREI_TEST_SKIP_ACL") == "1" {
|
||||
t.Skip("acl test skipped")
|
||||
if info.CanDegrade {
|
||||
name := filepath.Join(t.TempDir(), "check-degrade")
|
||||
if err := os.WriteFile(name, nil, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := acl.Update(name, os.Geteuid()); errors.Is(err, syscall.ENOTSUP) {
|
||||
t.Skip(err)
|
||||
}
|
||||
}
|
||||
|
||||
testFilePath := filepath.Join(t.TempDir(), testFileName)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !noskip
|
||||
|
||||
package info
|
||||
|
||||
// CanDegrade is whether tests are allowed to transparently degrade or skip due
|
||||
// to required system features being denied or unavailable.
|
||||
const CanDegrade = true
|
||||
@@ -0,0 +1,5 @@
|
||||
//go:build noskip
|
||||
|
||||
package info
|
||||
|
||||
const CanDegrade = false
|
||||
@@ -0,0 +1,90 @@
|
||||
package kobject
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/internal/uevent"
|
||||
)
|
||||
|
||||
// Event is a [uevent.Message] with known environment variables processed.
|
||||
type Event struct {
|
||||
// alloc_uevent_skb: action_string
|
||||
Action uevent.KobjectAction `json:"action"`
|
||||
// alloc_uevent_skb: devpath
|
||||
DevPath string `json:"devpath"`
|
||||
|
||||
// Uninterpreted environment variable pairs. An entry missing a separator
|
||||
// gains the value "\x00".
|
||||
Env map[string]string `json:"env"`
|
||||
|
||||
// SEQNUM value set by the kernel.
|
||||
Sequence uint64 `json:"seqnum"`
|
||||
// SYNTH_UUID value set on trigger, nil denotes a non-synthetic event.
|
||||
Synth *uevent.UUID `json:"synth_uuid,omitempty"`
|
||||
// SUBSYSTEM value set by the kernel.
|
||||
Subsystem string `json:"subsystem"`
|
||||
}
|
||||
|
||||
// Populate populates e with the contents of a [uevent.Message].
|
||||
//
|
||||
// The ACTION and DEVPATH environment variables are ignored and assumed to be
|
||||
// consistent with the header.
|
||||
func (e *Event) Populate(reportErr func(error), m *uevent.Message) {
|
||||
if reportErr == nil {
|
||||
reportErr = func(error) {}
|
||||
}
|
||||
|
||||
*e = Event{
|
||||
Action: m.Action,
|
||||
DevPath: m.DevPath,
|
||||
Env: make(map[string]string),
|
||||
}
|
||||
|
||||
for _, s := range m.Env {
|
||||
k, v, ok := strings.Cut(s, "=")
|
||||
if !ok {
|
||||
if _, ok = e.Env[s]; !ok {
|
||||
e.Env[s] = "\x00"
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch k {
|
||||
case "ACTION", "DEVPATH":
|
||||
continue
|
||||
|
||||
case "SEQNUM":
|
||||
seq, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
if _e := errors.Unwrap(err); _e != nil {
|
||||
err = _e
|
||||
}
|
||||
reportErr(err)
|
||||
|
||||
e.Env[k] = v
|
||||
continue
|
||||
}
|
||||
e.Sequence = seq
|
||||
|
||||
case "SYNTH_UUID":
|
||||
var uuid uevent.UUID
|
||||
err := uuid.UnmarshalText(unsafe.Slice(unsafe.StringData(v), len(v)))
|
||||
if err != nil {
|
||||
reportErr(err)
|
||||
|
||||
e.Env[k] = v
|
||||
continue
|
||||
}
|
||||
e.Synth = &uuid
|
||||
|
||||
case "SUBSYSTEM":
|
||||
e.Subsystem = v
|
||||
|
||||
default:
|
||||
e.Env[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package kobject_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/kobject"
|
||||
"hakurei.app/internal/uevent"
|
||||
)
|
||||
|
||||
func TestEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
msg uevent.Message
|
||||
want kobject.Event
|
||||
errs []error
|
||||
}{
|
||||
{"sample coldboot qemu", uevent.Message{
|
||||
Action: uevent.KOBJ_ADD,
|
||||
DevPath: "/devices/LNXSYSTM:00/LNXPWRBN:00",
|
||||
Env: []string{
|
||||
"ACTION=add",
|
||||
"DEVPATH=/devices/LNXSYSTM:00/LNXPWRBN:00",
|
||||
"SUBSYSTEM=acpi",
|
||||
"SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed",
|
||||
"MODALIAS=acpi:LNXPWRBN:",
|
||||
"SEQNUM=777",
|
||||
}}, kobject.Event{
|
||||
Action: uevent.KOBJ_ADD,
|
||||
DevPath: "/devices/LNXSYSTM:00/LNXPWRBN:00",
|
||||
Env: map[string]string{
|
||||
"MODALIAS": "acpi:LNXPWRBN:",
|
||||
},
|
||||
Sequence: 777,
|
||||
Synth: &uevent.UUID{
|
||||
0xfe, 0x4d, 0x7c, 0x9d,
|
||||
0xb8, 0xc6,
|
||||
0x4a, 0x70,
|
||||
0x9e, 0xf1,
|
||||
0x3d, 0x8a, 0x58, 0xd1, 0x8e, 0xed,
|
||||
},
|
||||
Subsystem: "acpi",
|
||||
}, []error{}},
|
||||
|
||||
{"nil reportErr", uevent.Message{Env: []string{
|
||||
"SEQNUM=\x00",
|
||||
}}, kobject.Event{Env: map[string]string{
|
||||
"SEQNUM": "\x00",
|
||||
}}, nil},
|
||||
|
||||
{"bad SEQNUM SYNTH_UUID", uevent.Message{Env: []string{
|
||||
"SEQNUM=\x00",
|
||||
"SYNTH_UUID=\x00",
|
||||
"SUBSYSTEM=\x00",
|
||||
}}, kobject.Event{Subsystem: "\x00", Env: map[string]string{
|
||||
"SEQNUM": "\x00",
|
||||
"SYNTH_UUID": "\x00",
|
||||
}}, []error{strconv.ErrSyntax, uevent.UUIDSizeError(1)}},
|
||||
|
||||
{"bad sep", uevent.Message{Env: []string{
|
||||
"SYNTH_UUID",
|
||||
}}, kobject.Event{Env: map[string]string{
|
||||
"SYNTH_UUID": "\x00",
|
||||
}}, []error{}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var f func(error)
|
||||
gotErrs := make([]error, 0)
|
||||
if tc.errs != nil {
|
||||
f = func(err error) {
|
||||
gotErrs = append(gotErrs, err)
|
||||
}
|
||||
}
|
||||
|
||||
var got kobject.Event
|
||||
got.Populate(f, &tc.msg)
|
||||
|
||||
if !reflect.DeepEqual(&got, &tc.want) {
|
||||
t.Errorf("Populate: %#v, want %#v", got, tc.want)
|
||||
}
|
||||
if tc.errs != nil && !reflect.DeepEqual(gotErrs, tc.errs) {
|
||||
t.Errorf("Populate: errs = %v, want %v", gotErrs, tc.errs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package container
|
||||
package landlock
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -14,11 +14,11 @@ const (
|
||||
LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
|
||||
)
|
||||
|
||||
// LandlockAccessFS is bitmask of handled filesystem actions.
|
||||
type LandlockAccessFS uint64
|
||||
// AccessFS is bitmask of handled filesystem actions.
|
||||
type AccessFS uint64
|
||||
|
||||
const (
|
||||
LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 1 << iota
|
||||
LANDLOCK_ACCESS_FS_EXECUTE AccessFS = 1 << iota
|
||||
LANDLOCK_ACCESS_FS_WRITE_FILE
|
||||
LANDLOCK_ACCESS_FS_READ_FILE
|
||||
LANDLOCK_ACCESS_FS_READ_DIR
|
||||
@@ -38,8 +38,8 @@ const (
|
||||
_LANDLOCK_ACCESS_FS_DELIM
|
||||
)
|
||||
|
||||
// String returns a space-separated string of [LandlockAccessFS] flags.
|
||||
func (f LandlockAccessFS) String() string {
|
||||
// String returns a space-separated string of [AccessFS] flags.
|
||||
func (f AccessFS) String() string {
|
||||
switch f {
|
||||
case LANDLOCK_ACCESS_FS_EXECUTE:
|
||||
return "execute"
|
||||
@@ -90,8 +90,8 @@ func (f LandlockAccessFS) String() string {
|
||||
return "fs_ioctl_dev"
|
||||
|
||||
default:
|
||||
var c []LandlockAccessFS
|
||||
for i := LandlockAccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
|
||||
var c []AccessFS
|
||||
for i := AccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
|
||||
if f&i != 0 {
|
||||
c = append(c, i)
|
||||
}
|
||||
@@ -107,18 +107,18 @@ func (f LandlockAccessFS) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// LandlockAccessNet is bitmask of handled network actions.
|
||||
type LandlockAccessNet uint64
|
||||
// AccessNet is bitmask of handled network actions.
|
||||
type AccessNet uint64
|
||||
|
||||
const (
|
||||
LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota
|
||||
LANDLOCK_ACCESS_NET_BIND_TCP AccessNet = 1 << iota
|
||||
LANDLOCK_ACCESS_NET_CONNECT_TCP
|
||||
|
||||
_LANDLOCK_ACCESS_NET_DELIM
|
||||
)
|
||||
|
||||
// String returns a space-separated string of [LandlockAccessNet] flags.
|
||||
func (f LandlockAccessNet) String() string {
|
||||
// String returns a space-separated string of [AccessNet] flags.
|
||||
func (f AccessNet) String() string {
|
||||
switch f {
|
||||
case LANDLOCK_ACCESS_NET_BIND_TCP:
|
||||
return "bind_tcp"
|
||||
@@ -127,8 +127,8 @@ func (f LandlockAccessNet) String() string {
|
||||
return "connect_tcp"
|
||||
|
||||
default:
|
||||
var c []LandlockAccessNet
|
||||
for i := LandlockAccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
|
||||
var c []AccessNet
|
||||
for i := AccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
|
||||
if f&i != 0 {
|
||||
c = append(c, i)
|
||||
}
|
||||
@@ -144,18 +144,18 @@ func (f LandlockAccessNet) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// LandlockScope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
|
||||
type LandlockScope uint64
|
||||
// Scope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
|
||||
type Scope uint64
|
||||
|
||||
const (
|
||||
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota
|
||||
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET Scope = 1 << iota
|
||||
LANDLOCK_SCOPE_SIGNAL
|
||||
|
||||
_LANDLOCK_SCOPE_DELIM
|
||||
)
|
||||
|
||||
// String returns a space-separated string of [LandlockScope] flags.
|
||||
func (f LandlockScope) String() string {
|
||||
// String returns a space-separated string of [Scope] flags.
|
||||
func (f Scope) String() string {
|
||||
switch f {
|
||||
case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET:
|
||||
return "abstract_unix_socket"
|
||||
@@ -164,8 +164,8 @@ func (f LandlockScope) String() string {
|
||||
return "signal"
|
||||
|
||||
default:
|
||||
var c []LandlockScope
|
||||
for i := LandlockScope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
|
||||
var c []Scope
|
||||
for i := Scope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
|
||||
if f&i != 0 {
|
||||
c = append(c, i)
|
||||
}
|
||||
@@ -184,12 +184,12 @@ func (f LandlockScope) String() string {
|
||||
// RulesetAttr is equivalent to struct landlock_ruleset_attr.
|
||||
type RulesetAttr struct {
|
||||
// Bitmask of handled filesystem actions.
|
||||
HandledAccessFS LandlockAccessFS
|
||||
HandledAccessFS AccessFS
|
||||
// Bitmask of handled network actions.
|
||||
HandledAccessNet LandlockAccessNet
|
||||
HandledAccessNet AccessNet
|
||||
// Bitmask of scopes restricting a Landlock domain from accessing outside
|
||||
// resources (e.g. IPCs).
|
||||
Scoped LandlockScope
|
||||
Scoped Scope
|
||||
}
|
||||
|
||||
// String returns a user-facing description of [RulesetAttr].
|
||||
@@ -239,13 +239,13 @@ func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
// LandlockGetABI returns the ABI version supported by the kernel.
|
||||
func LandlockGetABI() (int, error) {
|
||||
// GetABI returns the ABI version supported by the kernel.
|
||||
func GetABI() (int, error) {
|
||||
return (*RulesetAttr)(nil).Create(LANDLOCK_CREATE_RULESET_VERSION)
|
||||
}
|
||||
|
||||
// LandlockRestrictSelf applies a loaded ruleset to the calling thread.
|
||||
func LandlockRestrictSelf(rulesetFd int, flags uintptr) error {
|
||||
// RestrictSelf applies a loaded ruleset to the calling thread.
|
||||
func RestrictSelf(rulesetFd int, flags uintptr) error {
|
||||
r, _, errno := syscall.Syscall(
|
||||
ext.SYS_LANDLOCK_RESTRICT_SELF,
|
||||
uintptr(rulesetFd),
|
||||
@@ -0,0 +1,65 @@
|
||||
package landlock_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/internal/landlock"
|
||||
)
|
||||
|
||||
func TestLandlockString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
rulesetAttr *landlock.RulesetAttr
|
||||
want string
|
||||
}{
|
||||
{"nil", nil, "NULL"},
|
||||
{"zero", new(landlock.RulesetAttr), "0"},
|
||||
{"some", &landlock.RulesetAttr{Scoped: landlock.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
|
||||
{"set", &landlock.RulesetAttr{
|
||||
HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_MAKE_SYM | landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV | landlock.LANDLOCK_ACCESS_FS_WRITE_FILE,
|
||||
HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP,
|
||||
Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | landlock.LANDLOCK_SCOPE_SIGNAL,
|
||||
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
|
||||
{"all", &landlock.RulesetAttr{
|
||||
HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_EXECUTE |
|
||||
landlock.LANDLOCK_ACCESS_FS_WRITE_FILE |
|
||||
landlock.LANDLOCK_ACCESS_FS_READ_FILE |
|
||||
landlock.LANDLOCK_ACCESS_FS_READ_DIR |
|
||||
landlock.LANDLOCK_ACCESS_FS_REMOVE_DIR |
|
||||
landlock.LANDLOCK_ACCESS_FS_REMOVE_FILE |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_CHAR |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_DIR |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_REG |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_SOCK |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_FIFO |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_SYM |
|
||||
landlock.LANDLOCK_ACCESS_FS_REFER |
|
||||
landlock.LANDLOCK_ACCESS_FS_TRUNCATE |
|
||||
landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV,
|
||||
HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP |
|
||||
landlock.LANDLOCK_ACCESS_NET_CONNECT_TCP,
|
||||
Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
|
||||
landlock.LANDLOCK_SCOPE_SIGNAL,
|
||||
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := tc.rulesetAttr.String(); got != tc.want {
|
||||
t.Errorf("String: %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLandlockAttrSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := 24
|
||||
if got := unsafe.Sizeof(landlock.RulesetAttr{}); got != uintptr(want) {
|
||||
t.Errorf("Sizeof: %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -84,7 +85,7 @@ type syscallDispatcher interface {
|
||||
// setDumpable provides [container.SetDumpable].
|
||||
setDumpable(dumpable uintptr) error
|
||||
// receive provides [container.Receive].
|
||||
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
||||
receive(key string, e any, fdp *int) (closeFunc func() error, err error)
|
||||
|
||||
// containerStart provides the Start method of [container.Container].
|
||||
containerStart(z *container.Container) error
|
||||
@@ -154,8 +155,8 @@ func (direct) prctl(op, arg2, arg3 uintptr) error { return ext.Prctl(op, arg2, a
|
||||
func (direct) overflowUid(msg message.Msg) int { return container.OverflowUid(msg) }
|
||||
func (direct) overflowGid(msg message.Msg) int { return container.OverflowGid(msg) }
|
||||
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
|
||||
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
return container.Receive(key, e, fdp)
|
||||
func (direct) receive(key string, e any, fdp *int) (func() error, error) {
|
||||
return params.Receive(key, e, fdp)
|
||||
}
|
||||
|
||||
func (direct) containerStart(z *container.Container) error { return z.Start() }
|
||||
|
||||
@@ -401,12 +401,12 @@ func (k *kstub) setDumpable(dumpable uintptr) error {
|
||||
stub.CheckArg(k.Stub, "dumpable", dumpable, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
||||
func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, err error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("receive")
|
||||
reflect.ValueOf(e).Elem().Set(reflect.ValueOf(expect.Args[1]))
|
||||
if expect.Args[2] != nil {
|
||||
*fdp = expect.Args[2].(uintptr)
|
||||
*fdp = int(expect.Args[2].(uintptr))
|
||||
}
|
||||
return func() error { return k.Expects("closeReceive").Err }, expect.Error(
|
||||
stub.CheckArg(k.Stub, "key", key, 0))
|
||||
@@ -712,7 +712,7 @@ func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { pa
|
||||
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) setDumpable(uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) receive(string, any, *uintptr) (func() error, error) { panic("unreachable") }
|
||||
func (panicDispatcher) receive(string, any, *int) (func() error, error) { panic("unreachable") }
|
||||
func (panicDispatcher) containerStart(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) containerServe(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) containerWait(*container.Container) error { panic("unreachable") }
|
||||
|
||||
@@ -32,7 +32,14 @@ type outcome struct {
|
||||
syscallDispatcher
|
||||
}
|
||||
|
||||
func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, config *hst.Config) error {
|
||||
// finalise prepares an outcome for main.
|
||||
func (k *outcome) finalise(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
id *hst.ID,
|
||||
config *hst.Config,
|
||||
flags int,
|
||||
) error {
|
||||
if ctx == nil || id == nil {
|
||||
// unreachable
|
||||
panic("invalid call to finalise")
|
||||
@@ -43,7 +50,7 @@ func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, con
|
||||
}
|
||||
k.ctx = ctx
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
if err := config.Validate(flags); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ type outcomeStateSys struct {
|
||||
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
|
||||
appId string
|
||||
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
|
||||
et hst.Enablement
|
||||
et hst.Enablements
|
||||
|
||||
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
|
||||
directWayland bool
|
||||
|
||||
+15
-11
@@ -13,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/info"
|
||||
@@ -298,12 +297,12 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
|
||||
// accumulate enablements of remaining instances
|
||||
var (
|
||||
// alive enablement bits
|
||||
rt hst.Enablement
|
||||
rt hst.Enablements
|
||||
// alive instance count
|
||||
n int
|
||||
)
|
||||
for eh := range entries {
|
||||
var et hst.Enablement
|
||||
var et hst.Enablements
|
||||
if et, err = eh.Load(nil); err != nil {
|
||||
perror(err, "read state header of instance "+eh.ID.String())
|
||||
} else {
|
||||
@@ -372,17 +371,18 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
|
||||
// shim runs in the same session as monitor; see shim.go for behaviour
|
||||
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
|
||||
|
||||
var shimPipe *os.File
|
||||
if fd, w, err := container.Setup(&cmd.ExtraFiles); err != nil {
|
||||
var shimPipe [2]*os.File
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
return cmd, nil, &hst.AppError{Step: "create shim setup pipe", Err: err}
|
||||
} else {
|
||||
shimPipe = w
|
||||
cmd.Env = []string{
|
||||
// passed through to shim by hsu
|
||||
shimEnv + "=" + strconv.Itoa(fd),
|
||||
shimEnv + "=" + strconv.Itoa(3+len(cmd.ExtraFiles)),
|
||||
// interpreted by hsu
|
||||
"HAKUREI_IDENTITY=" + k.state.identity.String(),
|
||||
}
|
||||
cmd.ExtraFiles = append(cmd.ExtraFiles, r)
|
||||
shimPipe[0], shimPipe[1] = r, w
|
||||
}
|
||||
|
||||
if len(k.supp) > 0 {
|
||||
@@ -393,12 +393,16 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
|
||||
|
||||
msg.Verbosef("setuid helper at %s", hsuPath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
_, _ = shimPipe[0].Close(), shimPipe[1].Close()
|
||||
msg.Resume()
|
||||
return cmd, shimPipe, &hst.AppError{Step: "start setuid wrapper", Err: err}
|
||||
return cmd, nil, &hst.AppError{Step: "start setuid wrapper", Err: err}
|
||||
}
|
||||
if err := shimPipe[0].Close(); err != nil {
|
||||
msg.Verbose(err)
|
||||
}
|
||||
|
||||
*startTime = time.Now().UTC()
|
||||
return cmd, shimPipe, nil
|
||||
return cmd, shimPipe[1], nil
|
||||
}
|
||||
|
||||
// serveShim serves outcomeState through the shim setup pipe.
|
||||
@@ -411,11 +415,11 @@ func serveShim(msg message.Msg, shimPipe *os.File, state *outcomeState) error {
|
||||
msg.Verbose(err.Error())
|
||||
}
|
||||
if err := gob.NewEncoder(shimPipe).Encode(state); err != nil {
|
||||
_ = shimPipe.Close()
|
||||
msg.Resume()
|
||||
return &hst.AppError{Step: "transmit shim config", Err: err}
|
||||
}
|
||||
_ = shimPipe.Close()
|
||||
return nil
|
||||
return shimPipe.Close()
|
||||
}
|
||||
|
||||
// printMessageError prints the error message according to [message.GetMessage],
|
||||
|
||||
@@ -18,7 +18,13 @@ import (
|
||||
func IsPollDescriptor(fd uintptr) bool
|
||||
|
||||
// Main runs an app according to [hst.Config] and terminates. Main does not return.
|
||||
func Main(ctx context.Context, msg message.Msg, config *hst.Config, fd int) {
|
||||
func Main(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
config *hst.Config,
|
||||
flags int,
|
||||
fd int,
|
||||
) {
|
||||
// avoids runtime internals or standard streams
|
||||
if fd >= 0 {
|
||||
if IsPollDescriptor(uintptr(fd)) || fd < 3 {
|
||||
@@ -34,7 +40,7 @@ func Main(ctx context.Context, msg message.Msg, config *hst.Config, fd int) {
|
||||
k := outcome{syscallDispatcher: direct{msg}}
|
||||
|
||||
finaliseTime := time.Now()
|
||||
if err := k.finalise(ctx, msg, &id, config); err != nil {
|
||||
if err := k.finalise(ctx, msg, &id, config, flags); err != nil {
|
||||
printMessageError(msg.GetLogger().Fatalln, "cannot seal app:", err)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
@@ -427,7 +427,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
DirectPipeWire: true,
|
||||
|
||||
ID: "org.chromium.Chromium",
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Container: &hst.ContainerConfig{
|
||||
Env: nil,
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/internal/pipewire"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
@@ -197,7 +198,7 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
if errors.Is(err, syscall.EBADF) {
|
||||
k.fatal("invalid config descriptor")
|
||||
}
|
||||
if errors.Is(err, container.ErrReceiveEnv) {
|
||||
if errors.Is(err, params.ErrReceiveEnv) {
|
||||
k.fatal(shimEnv + " not set")
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/env"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/internal/stub"
|
||||
)
|
||||
|
||||
@@ -172,7 +173,7 @@ func TestShimEntrypoint(t *testing.T) {
|
||||
call("setDumpable", stub.ExpectArgs{uintptr(ext.SUID_DUMP_DISABLE)}, nil, nil),
|
||||
call("getppid", stub.ExpectArgs{}, 0xbad, nil),
|
||||
call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, container.ErrReceiveEnv),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, params.ErrReceiveEnv),
|
||||
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SHIM not set"}}, nil, nil),
|
||||
|
||||
// deferred
|
||||
|
||||
@@ -290,7 +290,9 @@ func (s *spFilesystemOp) toContainer(state *outcomeStateParams) error {
|
||||
if state.Container.Flags&hst.FDevice == 0 {
|
||||
state.params.Remount(fhs.AbsDev, syscall.MS_RDONLY)
|
||||
}
|
||||
if !state.as.NoRemountRoot {
|
||||
state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY)
|
||||
}
|
||||
|
||||
state.params.Env = make([]string, 0, len(state.env))
|
||||
for key, value := range state.env {
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
newConfig := func() *hst.Config {
|
||||
config := hst.Template()
|
||||
config.DirectPulse = true
|
||||
config.Enablements = hst.NewEnablements(hst.EPulse)
|
||||
config.Enablements = new(hst.EPulse)
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Package params provides helpers for receiving setup payload from parent.
|
||||
package params
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// ErrReceiveEnv is returned by [Receive] if setup fd is not present in environment.
|
||||
var ErrReceiveEnv = errors.New("environment variable not set")
|
||||
|
||||
// Receive retrieves setup fd from the environment and receives params.
|
||||
//
|
||||
// The file descriptor written to the value pointed to by fdp must not be passed
|
||||
// to any system calls. It is made available for ordering file descriptor only.
|
||||
func Receive(key string, v any, fdp *int) (func() error, error) {
|
||||
var setup *os.File
|
||||
|
||||
if s, ok := os.LookupEnv(key); !ok {
|
||||
return nil, ErrReceiveEnv
|
||||
} else {
|
||||
if fd, err := strconv.Atoi(s); err != nil {
|
||||
if _err := errors.Unwrap(err); _err != nil {
|
||||
err = _err
|
||||
}
|
||||
return nil, err
|
||||
} else {
|
||||
setup = os.NewFile(uintptr(fd), "setup")
|
||||
if setup == nil {
|
||||
return nil, syscall.EDOM
|
||||
}
|
||||
if fdp != nil {
|
||||
*fdp = fd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return setup.Close, gob.NewDecoder(setup).Decode(v)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package container_test
|
||||
package params_test
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal/params"
|
||||
)
|
||||
|
||||
func TestSetupReceive(t *testing.T) {
|
||||
@@ -30,8 +30,8 @@ func TestSetupReceive(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrReceiveEnv) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, container.ErrReceiveEnv)
|
||||
if _, err := params.Receive(key, nil, nil); !errors.Is(err, params.ErrReceiveEnv) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, params.ErrReceiveEnv)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestSetupReceive(t *testing.T) {
|
||||
const key = "TEST_ENV_FORMAT"
|
||||
t.Setenv(key, "")
|
||||
|
||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
|
||||
if _, err := params.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax)
|
||||
}
|
||||
})
|
||||
@@ -48,7 +48,7 @@ func TestSetupReceive(t *testing.T) {
|
||||
const key = "TEST_ENV_RANGE"
|
||||
t.Setenv(key, "-1")
|
||||
|
||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
|
||||
if _, err := params.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM)
|
||||
}
|
||||
})
|
||||
@@ -60,16 +60,22 @@ func TestSetupReceive(t *testing.T) {
|
||||
|
||||
encoderDone := make(chan error, 1)
|
||||
extraFiles := make([]*os.File, 0, 1)
|
||||
deadline, _ := t.Deadline()
|
||||
if fd, f, err := container.Setup(&extraFiles); err != nil {
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
t.Fatalf("Setup: error = %v", err)
|
||||
} else if fd != 3 {
|
||||
t.Fatalf("Setup: fd = %d, want 3", fd)
|
||||
} else {
|
||||
if err = f.SetDeadline(deadline); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
t.Cleanup(func() {
|
||||
if err = errors.Join(r.Close(), w.Close()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go func() { encoderDone <- gob.NewEncoder(f).Encode(payload) }()
|
||||
})
|
||||
|
||||
extraFiles = append(extraFiles, r)
|
||||
if deadline, ok := t.Deadline(); ok {
|
||||
if err = w.SetDeadline(deadline); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
go func() { encoderDone <- gob.NewEncoder(w).Encode(payload) }()
|
||||
}
|
||||
|
||||
if len(extraFiles) != 1 {
|
||||
@@ -87,13 +93,13 @@ func TestSetupReceive(t *testing.T) {
|
||||
|
||||
var (
|
||||
gotPayload []uint64
|
||||
fdp *uintptr
|
||||
fdp *int
|
||||
)
|
||||
if !useNilFdp {
|
||||
fdp = new(uintptr)
|
||||
fdp = new(int)
|
||||
}
|
||||
var closeFile func() error
|
||||
if f, err := container.Receive(key, &gotPayload, fdp); err != nil {
|
||||
if f, err := params.Receive(key, &gotPayload, fdp); err != nil {
|
||||
t.Fatalf("Receive: error = %v", err)
|
||||
} else {
|
||||
closeFile = f
|
||||
@@ -103,7 +109,7 @@ func TestSetupReceive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if !useNilFdp {
|
||||
if int(*fdp) != dupFd {
|
||||
if *fdp != dupFd {
|
||||
t.Errorf("Fd: %d, want %d", *fdp, dupFd)
|
||||
}
|
||||
}
|
||||
@@ -64,78 +64,6 @@ func TestFlatten(t *testing.T) {
|
||||
{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},
|
||||
|
||||
@@ -151,421 +79,6 @@ func TestFlatten(t *testing.T) {
|
||||
|
||||
{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) {
|
||||
|
||||
+82
-13
@@ -9,8 +9,10 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unique"
|
||||
@@ -27,6 +29,11 @@ import (
|
||||
// AbsWork is the container pathname [TContext.GetWorkDir] is mounted on.
|
||||
var AbsWork = fhs.AbsRoot.Append("work/")
|
||||
|
||||
// EnvJobs is the name of the environment variable holding a decimal
|
||||
// representation of the preferred job count. Its value must not affect cure
|
||||
// outcome.
|
||||
const EnvJobs = "CURE_JOBS"
|
||||
|
||||
// ExecPath is a slice of [Artifact] and the [check.Absolute] pathname to make
|
||||
// it available at under in the container.
|
||||
type ExecPath struct {
|
||||
@@ -89,6 +96,32 @@ 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
|
||||
}
|
||||
|
||||
const (
|
||||
// ExecTimeoutDefault replaces out of range [NewExec] timeout values.
|
||||
ExecTimeoutDefault = 15 * time.Minute
|
||||
@@ -105,6 +138,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
|
||||
|
||||
@@ -173,7 +208,7 @@ 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,
|
||||
@@ -188,13 +223,16 @@ 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 {
|
||||
return &a
|
||||
}
|
||||
@@ -206,6 +244,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)))
|
||||
@@ -252,11 +291,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()
|
||||
@@ -322,7 +376,7 @@ func readExecArtifact(r *IRReader, net bool) Artifact {
|
||||
}
|
||||
|
||||
return NewExec(
|
||||
name, checksumP, timeout, exclusive, dir, env, pathname, args, paths...,
|
||||
name, arch, checksumP, timeout, exclusive, dir, env, pathname, args, paths...,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -397,6 +451,7 @@ const SeccompPresets = std.PresetStrict &
|
||||
func (a *execArtifact) makeContainer(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
flags, jobs int,
|
||||
hostNet bool,
|
||||
temp, work *check.Absolute,
|
||||
getArtifact GetArtifactFunc,
|
||||
@@ -423,16 +478,30 @@ func (a *execArtifact) makeContainer(
|
||||
z.SeccompFlags |= seccomp.AllowMultiarch
|
||||
z.ParentPerm = 0700
|
||||
z.HostNet = hostNet
|
||||
z.HostAbstract = flags&CHostAbstract != 0
|
||||
z.Hostname = "cure"
|
||||
z.SetScheduler = flags&CSchedIdle != 0
|
||||
z.SchedPolicy = ext.SCHED_IDLE
|
||||
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.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args
|
||||
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 {
|
||||
@@ -559,6 +628,8 @@ func (c *Cache) EnterExec(
|
||||
var z *container.Container
|
||||
z, err = e.makeContainer(
|
||||
ctx, c.msg,
|
||||
c.flags,
|
||||
c.jobs,
|
||||
hostNet,
|
||||
temp, work,
|
||||
func(a Artifact) (*check.Absolute, unique.Handle[Checksum]) {
|
||||
@@ -598,14 +669,13 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
|
||||
msg := f.GetMessage()
|
||||
var z *container.Container
|
||||
if z, err = a.makeContainer(
|
||||
ctx, msg, hostNet,
|
||||
ctx, msg, f.cache.flags, f.GetJobs(), hostNet,
|
||||
f.GetTempDir(), f.GetWorkDir(),
|
||||
f.GetArtifact,
|
||||
f.cache.Ident,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
z.SetScheduler = f.cache.flags&CSchedIdle != 0
|
||||
|
||||
var status io.Writer
|
||||
if status, err = f.GetStatusWriter(); err != nil {
|
||||
@@ -621,12 +691,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{})
|
||||
@@ -641,6 +705,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)
|
||||
|
||||
+210
-32
@@ -1,36 +1,55 @@
|
||||
package pkg_test
|
||||
|
||||
//go:generate env CGO_ENABLED=0 go build -tags testtool -o testdata/testtool ./testdata
|
||||
|
||||
import (
|
||||
_ "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())
|
||||
|
||||
checkWithCache(t, []cacheTestCase{
|
||||
{"offline", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
@@ -38,7 +57,7 @@ func TestExec(t *testing.T) {
|
||||
|
||||
cureMany(t, c, []cureStep{
|
||||
{"container", pkg.NewExec(
|
||||
"exec-offline", nil, 0, false,
|
||||
"exec-offline", "", nil, 0, false,
|
||||
pkg.AbsWork,
|
||||
[]string{"HAKUREI_TEST=1"},
|
||||
check.MustAbs("/opt/bin/testtool"),
|
||||
@@ -58,10 +77,10 @@ func TestExec(t *testing.T) {
|
||||
},
|
||||
}),
|
||||
pkg.MustPath("/opt", false, testtool),
|
||||
), ignorePathname, wantChecksumOffline, nil},
|
||||
), ignorePathname, wantOffline, nil},
|
||||
|
||||
{"error passthrough", pkg.NewExec(
|
||||
"", nil, 0, true,
|
||||
"", "", nil, 0, true,
|
||||
pkg.AbsWork,
|
||||
[]string{"HAKUREI_TEST=1"},
|
||||
check.MustAbs("/opt/bin/testtool"),
|
||||
@@ -74,7 +93,7 @@ func TestExec(t *testing.T) {
|
||||
return stub.UniqueError(0xcafe)
|
||||
},
|
||||
}),
|
||||
), nil, pkg.Checksum{}, &pkg.DependencyCureError{
|
||||
), nil, nil, &pkg.DependencyCureError{
|
||||
{
|
||||
Ident: unique.Make(pkg.ID(pkg.MustDecode(
|
||||
"Sowo6oZRmG6xVtUaxB6bDWZhVsqAJsIJWUp0OPKlE103cY0lodx7dem8J-qQF0Z1",
|
||||
@@ -84,20 +103,20 @@ func TestExec(t *testing.T) {
|
||||
}},
|
||||
|
||||
{"invalid paths", pkg.NewExec(
|
||||
"", nil, 0, false,
|
||||
"", "", nil, 0, 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,
|
||||
"", "", nil, 0, false,
|
||||
pkg.AbsWork,
|
||||
nil,
|
||||
check.MustAbs("/opt/bin/testtool"),
|
||||
@@ -108,17 +127,35 @@ func TestExec(t *testing.T) {
|
||||
}
|
||||
|
||||
testtoolDestroy(t, base, c)
|
||||
}, pkg.MustDecode("Q5DluWQCAeohLoiGRImurwFp3vdz9IfQCoj7Fuhh73s4KQPRHpEQEnHTdNHmB8Fx")},
|
||||
}, 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/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
|
||||
"identifier/" + expected.Offline: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)},
|
||||
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
|
||||
|
||||
"temp": {Mode: fs.ModeDir | 0700},
|
||||
"work": {Mode: fs.ModeDir | 0700},
|
||||
}},
|
||||
|
||||
{"net", pkg.CValidateKnown, 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, false,
|
||||
pkg.AbsWork,
|
||||
[]string{"HAKUREI_TEST=1"},
|
||||
check.MustAbs("/opt/bin/testtool"),
|
||||
@@ -138,18 +175,34 @@ func TestExec(t *testing.T) {
|
||||
},
|
||||
}),
|
||||
pkg.MustPath("/opt", false, testtool),
|
||||
), ignorePathname, wantChecksum, nil},
|
||||
), ignorePathname, wantNet, nil},
|
||||
})
|
||||
|
||||
testtoolDestroy(t, base, c)
|
||||
}, pkg.MustDecode("bPYvvqxpfV7xcC1EptqyKNK1klLJgYHMDkzBcoOyK6j_Aj5hb0mXNPwTwPSK5F6Z")},
|
||||
}, expectsFS{
|
||||
".": {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/" + 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")},
|
||||
|
||||
"temp": {Mode: fs.ModeDir | 0700},
|
||||
"work": {Mode: fs.ModeDir | 0700},
|
||||
}},
|
||||
|
||||
{"overlay root", pkg.CValidateKnown, 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,
|
||||
pkg.AbsWork,
|
||||
[]string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"},
|
||||
check.MustAbs("/opt/bin/testtool"),
|
||||
@@ -163,18 +216,32 @@ func TestExec(t *testing.T) {
|
||||
},
|
||||
}),
|
||||
pkg.MustPath("/opt", false, testtool),
|
||||
), ignorePathname, wantChecksumOffline, nil},
|
||||
), ignorePathname, wantOffline, nil},
|
||||
})
|
||||
|
||||
testtoolDestroy(t, base, c)
|
||||
}, pkg.MustDecode("PO2DSSCa4yoSgEYRcCSZfQfwow1yRigL3Ry-hI0RDI4aGuFBha-EfXeSJnG_5_Rl")},
|
||||
}, 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/" + expected.OvlRoot: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)},
|
||||
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
|
||||
|
||||
"temp": {Mode: fs.ModeDir | 0700},
|
||||
"work": {Mode: fs.ModeDir | 0700},
|
||||
}},
|
||||
|
||||
{"overlay work", pkg.CValidateKnown, 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,
|
||||
pkg.AbsWork,
|
||||
[]string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"},
|
||||
check.MustAbs("/work/bin/testtool"),
|
||||
@@ -193,18 +260,32 @@ 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},
|
||||
})
|
||||
|
||||
testtoolDestroy(t, base, c)
|
||||
}, pkg.MustDecode("iaRt6l_Wm2n-h5UsDewZxQkCmjZjyL8r7wv32QT2kyV55-Lx09Dq4gfg9BiwPnKs")},
|
||||
}, 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/" + expected.Work: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)},
|
||||
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
|
||||
|
||||
"temp": {Mode: fs.ModeDir | 0700},
|
||||
"work": {Mode: fs.ModeDir | 0700},
|
||||
}},
|
||||
|
||||
{"multiple layers", pkg.CValidateKnown, 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,
|
||||
pkg.AbsWork,
|
||||
[]string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"},
|
||||
check.MustAbs("/opt/bin/testtool"),
|
||||
@@ -245,18 +326,37 @@ func TestExec(t *testing.T) {
|
||||
},
|
||||
}),
|
||||
pkg.MustPath("/opt", false, testtool),
|
||||
), ignorePathname, wantChecksumOffline, nil},
|
||||
), ignorePathname, wantOffline, nil},
|
||||
})
|
||||
|
||||
testtoolDestroy(t, base, c)
|
||||
}, pkg.MustDecode("O2YzyR7IUGU5J2CADy0hUZ3A5NkP_Vwzs4UadEdn2oMZZVWRtH0xZGJ3HXiimTnZ")},
|
||||
}, 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{}},
|
||||
"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")},
|
||||
|
||||
"temp": {Mode: fs.ModeDir | 0700},
|
||||
"work": {Mode: fs.ModeDir | 0700},
|
||||
}},
|
||||
|
||||
{"overlay layer promotion", pkg.CValidateKnown, 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, true,
|
||||
pkg.AbsWork,
|
||||
[]string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"},
|
||||
check.MustAbs("/opt/bin/testtool"),
|
||||
@@ -276,11 +376,89 @@ func TestExec(t *testing.T) {
|
||||
},
|
||||
}),
|
||||
pkg.MustPath("/opt", false, testtool),
|
||||
), ignorePathname, wantChecksumOffline, nil},
|
||||
), ignorePathname, wantOffline, nil},
|
||||
})
|
||||
|
||||
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)},
|
||||
|
||||
"temp": {Mode: fs.ModeDir | 0700},
|
||||
"work": {Mode: fs.ModeDir | 0700},
|
||||
}},
|
||||
|
||||
{"binfmt", pkg.CValidateKnown, 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, 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},
|
||||
})
|
||||
}, 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")},
|
||||
|
||||
"temp": {Mode: fs.ModeDir | 0700},
|
||||
"work": {Mode: fs.ModeDir | 0700},
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package pkg_test
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/check"
|
||||
@@ -10,18 +11,25 @@ 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")},
|
||||
|
||||
"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,10 @@
|
||||
package expected
|
||||
|
||||
const (
|
||||
Offline = "oe7Uv1u5BwxcuX3HLQzZRg1Q5oetJo6jWiKGMOeqLiqBkaVgyKzvx82N81_IzUAz"
|
||||
OvlRoot = "NacZGXwuRkTvcHaG08a22ujJ8qCWN0RSoFlRSR5FSt0ZcBbJ28FRvkYsHEtX7G8i"
|
||||
Layers = "WBJDrATtX6rIE5yAu8ePX3WmDF0Tt9kFiue0m3cRnyRoVx1my8a67fh3CAW486oP"
|
||||
Net = "CmYtj2sNB3LHtqiDuck_Lz3MjLLIiwyP8N4NDitQ1Icvv__LVP9p8tm-sHeQaKKp"
|
||||
Promote = "TX3eCloaQFkV-SZIH6Jg6E5WKH--rcXY1P0jnZKmLFKWrNqnOzd4G9eIBh6i5ywN"
|
||||
Work = "OuNiLSC68pZhAOr1YQ4WbV1tzASA0nxLEBcK7lO7MqxDY_j8dmP_C612RTuF23Lu"
|
||||
)
|
||||
@@ -9,18 +9,38 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"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())
|
||||
})
|
||||
|
||||
var hostNet, layers, promote bool
|
||||
if len(os.Args) == 2 && os.Args[0] == "testtool" {
|
||||
switch os.Args[1] {
|
||||
@@ -48,15 +68,15 @@ func main() {
|
||||
|
||||
var overlayRoot bool
|
||||
wantEnv := []string{"HAKUREI_TEST=1"}
|
||||
if len(os.Environ()) == 2 {
|
||||
if len(environ) == 2 {
|
||||
overlayRoot = true
|
||||
if !layers && !promote {
|
||||
log.SetPrefix("testtool(overlay root): ")
|
||||
}
|
||||
wantEnv = []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"}
|
||||
}
|
||||
if !slices.Equal(wantEnv, os.Environ()) {
|
||||
log.Fatalf("Environ: %q, want %q", os.Environ(), wantEnv)
|
||||
if !slices.Equal(wantEnv, environ) {
|
||||
log.Fatalf("Environ: %q, want %q", environ, wantEnv)
|
||||
}
|
||||
|
||||
var overlayWork bool
|
||||
@@ -142,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 != "/" {
|
||||
@@ -213,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")
|
||||
+52
-23
@@ -3,7 +3,6 @@ package pkg
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha512"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"io"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unique"
|
||||
"unsafe"
|
||||
@@ -39,22 +39,45 @@ func panicToError(errP *error) {
|
||||
}
|
||||
}
|
||||
|
||||
// irCache implements [IRCache].
|
||||
type irCache struct {
|
||||
// Artifact to [unique.Handle] of identifier cache.
|
||||
artifact sync.Map
|
||||
// Identifier free list, must not be accessed directly.
|
||||
identPool sync.Pool
|
||||
}
|
||||
|
||||
// zeroIRCache returns the initialised value of irCache.
|
||||
func zeroIRCache() irCache {
|
||||
return irCache{
|
||||
identPool: sync.Pool{New: func() any { return new(extIdent) }},
|
||||
}
|
||||
}
|
||||
|
||||
// IRCache provides memory management and caching primitives for IR and
|
||||
// identifier operations against [Artifact] implementations.
|
||||
//
|
||||
// The zero value is not safe for use.
|
||||
type IRCache struct{ irCache }
|
||||
|
||||
// NewIR returns the address of a new [IRCache].
|
||||
func NewIR() *IRCache {
|
||||
return &IRCache{zeroIRCache()}
|
||||
}
|
||||
|
||||
// IContext is passed to [Artifact.Params] and provides methods for writing
|
||||
// values to the IR writer. It does not expose the underlying [io.Writer].
|
||||
//
|
||||
// IContext is valid until [Artifact.Params] returns.
|
||||
type IContext struct {
|
||||
// Address of underlying [Cache], should be zeroed or made unusable after
|
||||
// Address of underlying irCache, should be zeroed or made unusable after
|
||||
// [Artifact.Params] returns and must not be exposed directly.
|
||||
cache *Cache
|
||||
ic *irCache
|
||||
// Written to by various methods, should be zeroed after [Artifact.Params]
|
||||
// returns and must not be exposed directly.
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying [context.Context].
|
||||
func (i *IContext) Unwrap() context.Context { return i.cache.ctx }
|
||||
|
||||
// irZero is a zero IR word.
|
||||
var irZero [wordSize]byte
|
||||
|
||||
@@ -136,11 +159,11 @@ func (i *IContext) mustWrite(p []byte) {
|
||||
// WriteIdent is not defined for an [Artifact] not part of the slice returned by
|
||||
// [Artifact.Dependencies].
|
||||
func (i *IContext) WriteIdent(a Artifact) {
|
||||
buf := i.cache.getIdentBuf()
|
||||
defer i.cache.putIdentBuf(buf)
|
||||
buf := i.ic.getIdentBuf()
|
||||
defer i.ic.putIdentBuf(buf)
|
||||
|
||||
IRKindIdent.encodeHeader(0).put(buf[:])
|
||||
*(*ID)(buf[wordSize:]) = i.cache.Ident(a).Value()
|
||||
*(*ID)(buf[wordSize:]) = i.ic.Ident(a).Value()
|
||||
i.mustWrite(buf[:])
|
||||
}
|
||||
|
||||
@@ -183,19 +206,19 @@ 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 (c *Cache) Encode(w io.Writer, a Artifact) (err error) {
|
||||
func (ic *irCache) Encode(w io.Writer, a Artifact) (err error) {
|
||||
deps := a.Dependencies()
|
||||
idents := make([]*extIdent, len(deps))
|
||||
for i, d := range deps {
|
||||
dbuf, did := c.unsafeIdent(d, true)
|
||||
dbuf, did := ic.unsafeIdent(d, true)
|
||||
if dbuf == nil {
|
||||
dbuf = c.getIdentBuf()
|
||||
dbuf = ic.getIdentBuf()
|
||||
binary.LittleEndian.PutUint64(dbuf[:], uint64(d.Kind()))
|
||||
*(*ID)(dbuf[wordSize:]) = did.Value()
|
||||
} else {
|
||||
c.storeIdent(d, dbuf)
|
||||
ic.storeIdent(d, dbuf)
|
||||
}
|
||||
defer c.putIdentBuf(dbuf)
|
||||
defer ic.putIdentBuf(dbuf)
|
||||
idents[i] = dbuf
|
||||
}
|
||||
slices.SortFunc(idents, func(a, b *extIdent) int {
|
||||
@@ -221,10 +244,10 @@ func (c *Cache) Encode(w io.Writer, a Artifact) (err error) {
|
||||
}
|
||||
|
||||
func() {
|
||||
i := IContext{c, w}
|
||||
i := IContext{ic, w}
|
||||
|
||||
defer panicToError(&err)
|
||||
defer func() { i.cache, i.w = nil, nil }()
|
||||
defer func() { i.ic, i.w = nil, nil }()
|
||||
|
||||
a.Params(&i)
|
||||
}()
|
||||
@@ -233,7 +256,7 @@ func (c *Cache) Encode(w io.Writer, a Artifact) (err error) {
|
||||
}
|
||||
|
||||
var f IREndFlag
|
||||
kcBuf := c.getIdentBuf()
|
||||
kcBuf := ic.getIdentBuf()
|
||||
sz := wordSize
|
||||
if kc, ok := a.(KnownChecksum); ok {
|
||||
f |= IREndKnownChecksum
|
||||
@@ -243,13 +266,13 @@ func (c *Cache) Encode(w io.Writer, a Artifact) (err error) {
|
||||
IRKindEnd.encodeHeader(uint32(f)).put(kcBuf[:])
|
||||
|
||||
_, err = w.Write(kcBuf[:sz])
|
||||
c.putIdentBuf(kcBuf)
|
||||
ic.putIdentBuf(kcBuf)
|
||||
return
|
||||
}
|
||||
|
||||
// encodeAll implements EncodeAll by recursively encoding dependencies and
|
||||
// performs deduplication by value via the encoded map.
|
||||
func (c *Cache) encodeAll(
|
||||
func (ic *irCache) encodeAll(
|
||||
w io.Writer,
|
||||
a Artifact,
|
||||
encoded map[Artifact]struct{},
|
||||
@@ -259,13 +282,13 @@ func (c *Cache) encodeAll(
|
||||
}
|
||||
|
||||
for _, d := range a.Dependencies() {
|
||||
if err = c.encodeAll(w, d, encoded); err != nil {
|
||||
if err = ic.encodeAll(w, d, encoded); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
encoded[a] = struct{}{}
|
||||
return c.Encode(w, a)
|
||||
return ic.Encode(w, a)
|
||||
}
|
||||
|
||||
// EncodeAll writes a self-describing IR stream of a to w and returns the first
|
||||
@@ -283,8 +306,8 @@ func (c *Cache) encodeAll(
|
||||
// the ident cache, nor does it contribute identifiers it computes back to the
|
||||
// ident cache. Because of this, multiple invocations of EncodeAll will have
|
||||
// similar cost and does not amortise when combined with a call to Cure.
|
||||
func (c *Cache) EncodeAll(w io.Writer, a Artifact) error {
|
||||
return c.encodeAll(w, a, make(map[Artifact]struct{}))
|
||||
func (ic *irCache) EncodeAll(w io.Writer, a Artifact) error {
|
||||
return ic.encodeAll(w, a, make(map[Artifact]struct{}))
|
||||
}
|
||||
|
||||
// ErrRemainingIR is returned for a [IRReadFunc] that failed to call
|
||||
@@ -409,6 +432,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")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package pkg_test
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/fs"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@@ -38,7 +39,7 @@ func TestIRRoundtrip(t *testing.T) {
|
||||
)},
|
||||
|
||||
{"exec offline", pkg.NewExec(
|
||||
"exec-offline", nil, 0, false,
|
||||
"exec-offline", "", nil, 0, false,
|
||||
pkg.AbsWork,
|
||||
[]string{"HAKUREI_TEST=1"},
|
||||
check.MustAbs("/opt/bin/testtool"),
|
||||
@@ -58,7 +59,7 @@ func TestIRRoundtrip(t *testing.T) {
|
||||
)},
|
||||
|
||||
{"exec net", pkg.NewExec(
|
||||
"exec-net",
|
||||
"exec-net", "",
|
||||
(*pkg.Checksum)(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
|
||||
0, false,
|
||||
pkg.AbsWork,
|
||||
@@ -105,9 +106,12 @@ 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},
|
||||
"work": {Mode: fs.ModeDir | 0700},
|
||||
},
|
||||
}
|
||||
}
|
||||
checkWithCache(t, testCasesCache)
|
||||
|
||||
+26
-22
@@ -3,12 +3,12 @@ package pkg_test
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"unique"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/internal/pkg"
|
||||
@@ -33,20 +33,14 @@ func TestHTTPGet(t *testing.T) {
|
||||
|
||||
checkWithCache(t, []cacheTestCase{
|
||||
{"direct", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
var r pkg.RContext
|
||||
rCacheVal := reflect.ValueOf(&r).Elem().FieldByName("cache")
|
||||
reflect.NewAt(
|
||||
rCacheVal.Type(),
|
||||
unsafe.Pointer(rCacheVal.UnsafeAddr()),
|
||||
).Elem().Set(reflect.ValueOf(c))
|
||||
|
||||
r := newRContext(t, c)
|
||||
f := pkg.NewHTTPGet(
|
||||
&client,
|
||||
"file:///testdata",
|
||||
testdataChecksum.Value(),
|
||||
)
|
||||
var got []byte
|
||||
if rc, err := f.Cure(&r); err != nil {
|
||||
if rc, err := f.Cure(r); err != nil {
|
||||
t.Fatalf("Cure: error = %v", err)
|
||||
} else if got, err = io.ReadAll(rc); err != nil {
|
||||
t.Fatalf("ReadAll: error = %v", err)
|
||||
@@ -65,7 +59,7 @@ func TestHTTPGet(t *testing.T) {
|
||||
wantErrMismatch := &pkg.ChecksumMismatchError{
|
||||
Got: testdataChecksum.Value(),
|
||||
}
|
||||
if rc, err := f.Cure(&r); err != nil {
|
||||
if rc, err := f.Cure(r); err != nil {
|
||||
t.Fatalf("Cure: error = %v", err)
|
||||
} else if got, err = io.ReadAll(rc); err != nil {
|
||||
t.Fatalf("ReadAll: error = %v", err)
|
||||
@@ -76,7 +70,7 @@ func TestHTTPGet(t *testing.T) {
|
||||
}
|
||||
|
||||
// check fallback validation
|
||||
if rc, err := f.Cure(&r); err != nil {
|
||||
if rc, err := f.Cure(r); err != nil {
|
||||
t.Fatalf("Cure: error = %v", err)
|
||||
} else if err = rc.Close(); !reflect.DeepEqual(err, wantErrMismatch) {
|
||||
t.Fatalf("Close: error = %#v, want %#v", err, wantErrMismatch)
|
||||
@@ -89,18 +83,18 @@ func TestHTTPGet(t *testing.T) {
|
||||
pkg.Checksum{},
|
||||
)
|
||||
wantErrNotFound := pkg.ResponseStatusError(http.StatusNotFound)
|
||||
if _, err := f.Cure(&r); !reflect.DeepEqual(err, wantErrNotFound) {
|
||||
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},
|
||||
"work": {Mode: fs.ModeDir | 0700},
|
||||
}},
|
||||
|
||||
{"cure", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
var r pkg.RContext
|
||||
rCacheVal := reflect.ValueOf(&r).Elem().FieldByName("cache")
|
||||
reflect.NewAt(
|
||||
rCacheVal.Type(),
|
||||
unsafe.Pointer(rCacheVal.UnsafeAddr()),
|
||||
).Elem().Set(reflect.ValueOf(c))
|
||||
r := newRContext(t, c)
|
||||
|
||||
f := pkg.NewHTTPGet(
|
||||
&client,
|
||||
@@ -120,7 +114,7 @@ func TestHTTPGet(t *testing.T) {
|
||||
}
|
||||
|
||||
var got []byte
|
||||
if rc, err := f.Cure(&r); err != nil {
|
||||
if rc, err := f.Cure(r); err != nil {
|
||||
t.Fatalf("Cure: error = %v", err)
|
||||
} else if got, err = io.ReadAll(rc); err != nil {
|
||||
t.Fatalf("ReadAll: error = %v", err)
|
||||
@@ -136,7 +130,7 @@ func TestHTTPGet(t *testing.T) {
|
||||
"file:///testdata",
|
||||
testdataChecksum.Value(),
|
||||
)
|
||||
if rc, err := f.Cure(&r); err != nil {
|
||||
if rc, err := f.Cure(r); err != nil {
|
||||
t.Fatalf("Cure: error = %v", err)
|
||||
} else if got, err = io.ReadAll(rc); err != nil {
|
||||
t.Fatalf("ReadAll: error = %v", err)
|
||||
@@ -156,6 +150,16 @@ 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")},
|
||||
|
||||
"work": {Mode: fs.ModeDir | 0700},
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
+338
-62
@@ -18,6 +18,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -70,8 +71,70 @@ func MustDecode(s string) (checksum Checksum) {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
// extension is a string uniquely identifying a set of custom [Artifact]
|
||||
// implementations registered by calling [Register].
|
||||
extension string
|
||||
|
||||
// openMu synchronises access to global state for initialisation.
|
||||
openMu sync.Mutex
|
||||
// opened is false if [Open] was never called.
|
||||
opened bool
|
||||
)
|
||||
|
||||
// Extension returns a string uniquely identifying the currently registered set
|
||||
// of custom [Artifact], or the zero value if none was registered.
|
||||
func Extension() string { return extension }
|
||||
|
||||
// ValidExtension returns whether s is valid for use in a call to SetExtension.
|
||||
func ValidExtension(s string) bool {
|
||||
if l := len(s); l == 0 || l > 128 {
|
||||
return false
|
||||
}
|
||||
for _, v := range s {
|
||||
if v < 'a' || v > 'z' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ErrInvalidExtension is returned for a variant identification string for which
|
||||
// [ValidExtension] returns false.
|
||||
var ErrInvalidExtension = errors.New("invalid extension variant identification string")
|
||||
|
||||
// SetExtension sets the extension variant identification string. SetExtension
|
||||
// must be called before [Open] if custom [Artifact] implementations had been
|
||||
// recorded by calling [Register].
|
||||
//
|
||||
// The variant identification string must be between 1 and 128 bytes long and
|
||||
// consists of only bytes between 'a' and 'z'.
|
||||
//
|
||||
// SetExtension is not safe for concurrent use. SetExtension is called at most
|
||||
// once and must not be called after the first instance of Cache has been opened.
|
||||
func SetExtension(s string) {
|
||||
openMu.Lock()
|
||||
defer openMu.Unlock()
|
||||
|
||||
if opened {
|
||||
panic("attempting to set extension after open")
|
||||
}
|
||||
if extension != "" {
|
||||
panic("attempting to set extension twice")
|
||||
}
|
||||
if !ValidExtension(s) {
|
||||
panic(ErrInvalidExtension)
|
||||
}
|
||||
extension = s
|
||||
statusHeader = makeStatusHeader(s)
|
||||
}
|
||||
|
||||
// common holds elements and receives methods shared between different contexts.
|
||||
type common struct {
|
||||
// Context specific to this [Artifact]. The toplevel context in [Cache] must
|
||||
// not be exposed directly.
|
||||
ctx context.Context
|
||||
|
||||
// Address of underlying [Cache], should be zeroed or made unusable after
|
||||
// Cure returns and must not be exposed directly.
|
||||
cache *Cache
|
||||
@@ -98,19 +161,27 @@ type TContext struct {
|
||||
common
|
||||
}
|
||||
|
||||
// statusHeader is the header written to all status files in dirStatus.
|
||||
var statusHeader = func() string {
|
||||
// makeStatusHeader creates the header written to every status file. This should
|
||||
// not be called directly, its result is stored in statusHeader and will not
|
||||
// change after the first [Cache] is opened.
|
||||
func makeStatusHeader(extension string) string {
|
||||
s := programName
|
||||
if v := info.Version(); v != info.FallbackVersion {
|
||||
s += " " + v
|
||||
}
|
||||
if extension != "" {
|
||||
s += " with " + extension + " extensions"
|
||||
}
|
||||
s += " (" + runtime.GOARCH + ")"
|
||||
if name, err := os.Hostname(); err == nil {
|
||||
s += " on " + name
|
||||
}
|
||||
s += "\n\n"
|
||||
return s
|
||||
}()
|
||||
}
|
||||
|
||||
// statusHeader is the header written to all status files in dirStatus.
|
||||
var statusHeader = makeStatusHeader("")
|
||||
|
||||
// prepareStatus initialises the status file once.
|
||||
func (t *TContext) prepareStatus() error {
|
||||
@@ -183,11 +254,15 @@ func (t *TContext) destroy(errP *error) {
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying [context.Context].
|
||||
func (c *common) Unwrap() context.Context { return c.cache.ctx }
|
||||
func (c *common) Unwrap() context.Context { return c.ctx }
|
||||
|
||||
// GetMessage returns [message.Msg] held by the underlying [Cache].
|
||||
func (c *common) GetMessage() message.Msg { return c.cache.msg }
|
||||
|
||||
// GetJobs returns the preferred number of jobs to run, when applicable. Its
|
||||
// value must not affect cure outcome.
|
||||
func (c *common) GetJobs() int { return c.cache.jobs }
|
||||
|
||||
// GetWorkDir returns a pathname to a directory which [Artifact] is expected to
|
||||
// write its output to. This is not the final resting place of the [Artifact]
|
||||
// and this pathname should not be directly referred to in the final contents.
|
||||
@@ -207,11 +282,11 @@ func (t *TContext) GetTempDir() *check.Absolute { return t.temp }
|
||||
// [ChecksumMismatchError], or the underlying implementation may block on Close.
|
||||
func (c *common) Open(a Artifact) (r io.ReadCloser, err error) {
|
||||
if f, ok := a.(FileArtifact); ok {
|
||||
return c.cache.openFile(f)
|
||||
return c.cache.openFile(c.ctx, f)
|
||||
}
|
||||
|
||||
var pathname *check.Absolute
|
||||
if pathname, _, err = c.cache.Cure(a); err != nil {
|
||||
if pathname, _, err = c.cache.cure(a, true); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -372,6 +447,9 @@ type KnownChecksum interface {
|
||||
}
|
||||
|
||||
// FileArtifact refers to an [Artifact] backed by a single file.
|
||||
//
|
||||
// FileArtifact does not support fine-grained cancellation. Its context is
|
||||
// inherited from the first [TrivialArtifact] or [FloodArtifact] that opens it.
|
||||
type FileArtifact interface {
|
||||
// Cure returns [io.ReadCloser] of the full contents of [FileArtifact]. If
|
||||
// [FileArtifact] implements [KnownChecksum], Cure is responsible for
|
||||
@@ -416,6 +494,9 @@ const (
|
||||
// KindFile is the kind of [Artifact] returned by [NewFile].
|
||||
KindFile
|
||||
|
||||
// _kindEnd is the total number of kinds and does not denote a kind.
|
||||
_kindEnd
|
||||
|
||||
// KindCustomOffset is the first [Kind] value reserved for implementations
|
||||
// not from this package.
|
||||
KindCustomOffset = 1 << 31
|
||||
@@ -430,6 +511,9 @@ const (
|
||||
// fileLock is the file name appended to Cache.base for guaranteeing
|
||||
// exclusive access to the cache directory.
|
||||
fileLock = "lock"
|
||||
// fileVariant is the file name appended to Cache.base holding the variant
|
||||
// identification string set by a prior call to [SetExtension].
|
||||
fileVariant = "variant"
|
||||
|
||||
// dirIdentifier is the directory name appended to Cache.base for storing
|
||||
// artifacts named after their [ID].
|
||||
@@ -521,8 +605,48 @@ const (
|
||||
// was caused by an incorrect checksum accidentally left behind while
|
||||
// bumping a package. Only enable this if you are really sure you need it.
|
||||
CAssumeChecksum
|
||||
|
||||
// CHostAbstract disables restriction of sandboxed processes from connecting
|
||||
// to an abstract UNIX socket created by a host process.
|
||||
//
|
||||
// This is considered less secure in some systems, but does not introduce
|
||||
// impurity due to [KindExecNet] being [KnownChecksum]. This flag exists
|
||||
// to support kernels without Landlock LSM enabled.
|
||||
CHostAbstract
|
||||
|
||||
// CPromoteVariant allows [pkg.Open] to promote an unextended on-disk cache
|
||||
// to the current extension variant. This is a one-way operation.
|
||||
CPromoteVariant
|
||||
|
||||
// CSuppressInit arranges for verbose output of the container init to be
|
||||
// suppressed regardless of [message.Msg] state.
|
||||
CSuppressInit
|
||||
)
|
||||
|
||||
// toplevel holds [context.WithCancel] over caller-supplied context, where all
|
||||
// [Artifact] context are derived from.
|
||||
type toplevel struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// newToplevel returns the address of a new toplevel via ctx.
|
||||
func newToplevel(ctx context.Context) *toplevel {
|
||||
var t toplevel
|
||||
t.ctx, t.cancel = context.WithCancel(ctx)
|
||||
return &t
|
||||
}
|
||||
|
||||
// pendingCure provides synchronisation and cancellation for pending cures.
|
||||
type pendingCure struct {
|
||||
// Closed on cure completion.
|
||||
done <-chan struct{}
|
||||
// Error outcome, safe to access after done is closed.
|
||||
err error
|
||||
// Cancels the corresponding cure.
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// Cache is a support layer that implementations of [Artifact] can use to store
|
||||
// cured [Artifact] data in a content addressed fashion.
|
||||
type Cache struct {
|
||||
@@ -530,11 +654,10 @@ type Cache struct {
|
||||
// implementation and receives an equal amount of elements after.
|
||||
cures chan struct{}
|
||||
|
||||
// [context.WithCancel] over caller-supplied context, used by [Artifact] and
|
||||
// all dependency curing goroutines.
|
||||
ctx context.Context
|
||||
// Cancels ctx.
|
||||
cancel context.CancelFunc
|
||||
// Parent context which toplevel was derived from.
|
||||
parent context.Context
|
||||
// For deriving curing context, must not be accessed directly.
|
||||
toplevel atomic.Pointer[toplevel]
|
||||
// For waiting on dependency curing goroutines.
|
||||
wg sync.WaitGroup
|
||||
// Reports new cures and passed to [Artifact].
|
||||
@@ -544,11 +667,11 @@ type Cache struct {
|
||||
base *check.Absolute
|
||||
// Immutable cure options set by [Open].
|
||||
flags int
|
||||
// Immutable job count, when applicable.
|
||||
jobs int
|
||||
|
||||
// Artifact to [unique.Handle] of identifier cache.
|
||||
artifact sync.Map
|
||||
// Identifier free list, must not be accessed directly.
|
||||
identPool sync.Pool
|
||||
// Must not be exposed directly.
|
||||
irCache
|
||||
|
||||
// Synchronises access to dirChecksum.
|
||||
checksumMu sync.RWMutex
|
||||
@@ -558,9 +681,11 @@ type Cache struct {
|
||||
// Identifier to error pair for unrecoverably faulted [Artifact].
|
||||
identErr map[unique.Handle[ID]]error
|
||||
// Pending identifiers, accessed through Cure for entries not in ident.
|
||||
identPending map[unique.Handle[ID]]<-chan struct{}
|
||||
identPending map[unique.Handle[ID]]*pendingCure
|
||||
// Synchronises access to ident and corresponding filesystem entries.
|
||||
identMu sync.RWMutex
|
||||
// Synchronises entry into Abort and Cure.
|
||||
abortMu sync.RWMutex
|
||||
|
||||
// Synchronises entry into exclusive artifacts for the cure method.
|
||||
exclMu sync.Mutex
|
||||
@@ -569,8 +694,10 @@ type Cache struct {
|
||||
|
||||
// Unlocks the on-filesystem cache. Must only be called from Close.
|
||||
unlock func()
|
||||
// Synchronises calls to Close.
|
||||
closeOnce sync.Once
|
||||
// Whether [Cache] is considered closed.
|
||||
closed bool
|
||||
// Synchronises calls to Abort and Close.
|
||||
closeMu sync.Mutex
|
||||
|
||||
// Whether EnterExec has not yet returned.
|
||||
inExec atomic.Bool
|
||||
@@ -580,24 +707,24 @@ type Cache struct {
|
||||
type extIdent [wordSize + len(ID{})]byte
|
||||
|
||||
// getIdentBuf returns the address of an extIdent for Ident.
|
||||
func (c *Cache) getIdentBuf() *extIdent { return c.identPool.Get().(*extIdent) }
|
||||
func (ic *irCache) getIdentBuf() *extIdent { return ic.identPool.Get().(*extIdent) }
|
||||
|
||||
// putIdentBuf adds buf to identPool.
|
||||
func (c *Cache) putIdentBuf(buf *extIdent) { c.identPool.Put(buf) }
|
||||
func (ic *irCache) putIdentBuf(buf *extIdent) { ic.identPool.Put(buf) }
|
||||
|
||||
// storeIdent adds an [Artifact] to the artifact cache.
|
||||
func (c *Cache) storeIdent(a Artifact, buf *extIdent) unique.Handle[ID] {
|
||||
func (ic *irCache) storeIdent(a Artifact, buf *extIdent) unique.Handle[ID] {
|
||||
idu := unique.Make(ID(buf[wordSize:]))
|
||||
c.artifact.Store(a, idu)
|
||||
ic.artifact.Store(a, idu)
|
||||
return idu
|
||||
}
|
||||
|
||||
// Ident returns the identifier of an [Artifact].
|
||||
func (c *Cache) Ident(a Artifact) unique.Handle[ID] {
|
||||
buf, idu := c.unsafeIdent(a, false)
|
||||
func (ic *irCache) Ident(a Artifact) unique.Handle[ID] {
|
||||
buf, idu := ic.unsafeIdent(a, false)
|
||||
if buf != nil {
|
||||
idu = c.storeIdent(a, buf)
|
||||
c.putIdentBuf(buf)
|
||||
idu = ic.storeIdent(a, buf)
|
||||
ic.putIdentBuf(buf)
|
||||
}
|
||||
return idu
|
||||
}
|
||||
@@ -605,17 +732,17 @@ func (c *Cache) Ident(a Artifact) unique.Handle[ID] {
|
||||
// unsafeIdent implements Ident but returns the underlying buffer for a newly
|
||||
// computed identifier. Callers must return this buffer to identPool. encodeKind
|
||||
// is only a hint, kind may still be encoded in the buffer.
|
||||
func (c *Cache) unsafeIdent(a Artifact, encodeKind bool) (
|
||||
func (ic *irCache) unsafeIdent(a Artifact, encodeKind bool) (
|
||||
buf *extIdent,
|
||||
idu unique.Handle[ID],
|
||||
) {
|
||||
if id, ok := c.artifact.Load(a); ok {
|
||||
if id, ok := ic.artifact.Load(a); ok {
|
||||
idu = id.(unique.Handle[ID])
|
||||
return
|
||||
}
|
||||
|
||||
if ki, ok := a.(KnownIdent); ok {
|
||||
buf = c.getIdentBuf()
|
||||
buf = ic.getIdentBuf()
|
||||
if encodeKind {
|
||||
binary.LittleEndian.PutUint64(buf[:], uint64(a.Kind()))
|
||||
}
|
||||
@@ -623,9 +750,9 @@ func (c *Cache) unsafeIdent(a Artifact, encodeKind bool) (
|
||||
return
|
||||
}
|
||||
|
||||
buf = c.getIdentBuf()
|
||||
buf = ic.getIdentBuf()
|
||||
h := sha512.New384()
|
||||
if err := c.Encode(h, a); err != nil {
|
||||
if err := ic.Encode(h, a); err != nil {
|
||||
// unreachable
|
||||
panic(err)
|
||||
}
|
||||
@@ -994,7 +1121,11 @@ func (c *Cache) Scrub(checks int) error {
|
||||
// loadOrStoreIdent attempts to load a cached [Artifact] by its identifier or
|
||||
// wait for a pending [Artifact] to cure. If neither is possible, the current
|
||||
// identifier is stored in identPending and a non-nil channel is returned.
|
||||
//
|
||||
// Since identErr is treated as grow-only, loadOrStoreIdent must not be entered
|
||||
// without holding a read lock on abortMu.
|
||||
func (c *Cache) loadOrStoreIdent(id unique.Handle[ID]) (
|
||||
ctx context.Context,
|
||||
done chan<- struct{},
|
||||
checksum unique.Handle[Checksum],
|
||||
err error,
|
||||
@@ -1011,20 +1142,23 @@ func (c *Cache) loadOrStoreIdent(id unique.Handle[ID]) (
|
||||
return
|
||||
}
|
||||
|
||||
var notify <-chan struct{}
|
||||
if notify, ok = c.identPending[id]; ok {
|
||||
var pending *pendingCure
|
||||
if pending, ok = c.identPending[id]; ok {
|
||||
c.identMu.Unlock()
|
||||
<-notify
|
||||
<-pending.done
|
||||
c.identMu.RLock()
|
||||
if checksum, ok = c.ident[id]; !ok {
|
||||
err = c.identErr[id]
|
||||
err = pending.err
|
||||
}
|
||||
c.identMu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
d := make(chan struct{})
|
||||
c.identPending[id] = d
|
||||
pending = &pendingCure{done: d}
|
||||
ctx, pending.cancel = context.WithCancel(c.toplevel.Load().ctx)
|
||||
c.wg.Add(1)
|
||||
c.identPending[id] = pending
|
||||
c.identMu.Unlock()
|
||||
done = d
|
||||
return
|
||||
@@ -1040,21 +1174,62 @@ func (c *Cache) finaliseIdent(
|
||||
) {
|
||||
c.identMu.Lock()
|
||||
if err != nil {
|
||||
c.identPending[id].err = err
|
||||
c.identErr[id] = err
|
||||
} else {
|
||||
c.ident[id] = checksum
|
||||
}
|
||||
delete(c.identPending, id)
|
||||
c.identMu.Unlock()
|
||||
c.wg.Done()
|
||||
|
||||
close(done)
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the ongoing cure of an [Artifact]
|
||||
// referred to by the specified identifier completes. Done may return nil if
|
||||
// no ongoing cure of the specified identifier exists.
|
||||
func (c *Cache) Done(id unique.Handle[ID]) <-chan struct{} {
|
||||
c.identMu.RLock()
|
||||
pending, ok := c.identPending[id]
|
||||
c.identMu.RUnlock()
|
||||
if !ok || pending == nil {
|
||||
return nil
|
||||
}
|
||||
return pending.done
|
||||
}
|
||||
|
||||
// Cancel cancels the ongoing cure of an [Artifact] referred to by the specified
|
||||
// identifier. Cancel returns whether the [context.CancelFunc] has been killed.
|
||||
// Cancel returns after the cure is complete.
|
||||
func (c *Cache) Cancel(id unique.Handle[ID]) bool {
|
||||
c.identMu.RLock()
|
||||
pending, ok := c.identPending[id]
|
||||
c.identMu.RUnlock()
|
||||
if !ok || pending == nil || pending.cancel == nil {
|
||||
return false
|
||||
}
|
||||
pending.cancel()
|
||||
<-pending.done
|
||||
|
||||
c.abortMu.Lock()
|
||||
c.identMu.Lock()
|
||||
delete(c.identErr, id)
|
||||
c.identMu.Unlock()
|
||||
c.abortMu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
// openFile tries to load [FileArtifact] from [Cache], and if that fails,
|
||||
// obtains it via [FileArtifact.Cure] instead. Notably, it does not cure
|
||||
// [FileArtifact] to the filesystem. If err is nil, the caller is responsible
|
||||
// for closing the resulting [io.ReadCloser].
|
||||
func (c *Cache) openFile(f FileArtifact) (r io.ReadCloser, err error) {
|
||||
//
|
||||
// The context must originate from loadOrStoreIdent to enable cancellation.
|
||||
func (c *Cache) openFile(
|
||||
ctx context.Context,
|
||||
f FileArtifact,
|
||||
) (r io.ReadCloser, err error) {
|
||||
if kc, ok := f.(KnownChecksum); c.flags&CAssumeChecksum != 0 && ok {
|
||||
c.checksumMu.RLock()
|
||||
r, err = os.Open(c.base.Append(
|
||||
@@ -1085,7 +1260,7 @@ func (c *Cache) openFile(f FileArtifact) (r io.ReadCloser, err error) {
|
||||
}
|
||||
}()
|
||||
}
|
||||
return f.Cure(&RContext{common{c}})
|
||||
return f.Cure(&RContext{common{ctx, c}})
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1233,12 +1408,11 @@ func (c *Cache) Cure(a Artifact) (
|
||||
checksum unique.Handle[Checksum],
|
||||
err error,
|
||||
) {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
err = c.ctx.Err()
|
||||
return
|
||||
c.abortMu.RLock()
|
||||
defer c.abortMu.RUnlock()
|
||||
|
||||
default:
|
||||
if err = c.toplevel.Load().ctx.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return c.cure(a, true)
|
||||
@@ -1324,15 +1498,16 @@ func (c *Cache) enterCure(a Artifact, curesExempt bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := c.toplevel.Load().ctx
|
||||
select {
|
||||
case c.cures <- struct{}{}:
|
||||
return nil
|
||||
|
||||
case <-c.ctx.Done():
|
||||
case <-ctx.Done():
|
||||
if a.IsExclusive() {
|
||||
c.exclMu.Unlock()
|
||||
}
|
||||
return c.ctx.Err()
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1430,7 +1605,8 @@ func (r *RContext) NewMeasuredReader(
|
||||
return r.cache.newMeasuredReader(rc, checksum)
|
||||
}
|
||||
|
||||
// cure implements Cure without checking the full dependency graph.
|
||||
// cure implements Cure without acquiring a read lock on abortMu. cure must not
|
||||
// be entered during Abort.
|
||||
func (c *Cache) cure(a Artifact, curesExempt bool) (
|
||||
pathname *check.Absolute,
|
||||
checksum unique.Handle[Checksum],
|
||||
@@ -1449,8 +1625,11 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
|
||||
}
|
||||
}()
|
||||
|
||||
var done chan<- struct{}
|
||||
done, checksum, err = c.loadOrStoreIdent(id)
|
||||
var (
|
||||
ctx context.Context
|
||||
done chan<- struct{}
|
||||
)
|
||||
ctx, done, checksum, err = c.loadOrStoreIdent(id)
|
||||
if done == nil {
|
||||
return
|
||||
} else {
|
||||
@@ -1563,7 +1742,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
|
||||
if err = c.enterCure(a, curesExempt); err != nil {
|
||||
return
|
||||
}
|
||||
r, err = f.Cure(&RContext{common{c}})
|
||||
r, err = f.Cure(&RContext{common{ctx, c}})
|
||||
if err == nil {
|
||||
if checksumPathname == nil || c.flags&CValidateKnown != 0 {
|
||||
h := sha512.New384()
|
||||
@@ -1643,7 +1822,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
|
||||
c.base.Append(dirWork, ids),
|
||||
c.base.Append(dirTemp, ids),
|
||||
ids, nil, nil, nil,
|
||||
common{c},
|
||||
common{ctx, c},
|
||||
}
|
||||
switch ca := a.(type) {
|
||||
case TrivialArtifact:
|
||||
@@ -1794,23 +1973,65 @@ func (c *Cache) OpenStatus(a Artifact) (r io.ReadSeekCloser, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Abort cancels all pending cures and waits for them to clean up, but does not
|
||||
// close the cache.
|
||||
func (c *Cache) Abort() {
|
||||
c.closeMu.Lock()
|
||||
defer c.closeMu.Unlock()
|
||||
|
||||
if c.closed {
|
||||
return
|
||||
}
|
||||
|
||||
c.toplevel.Load().cancel()
|
||||
c.abortMu.Lock()
|
||||
defer c.abortMu.Unlock()
|
||||
|
||||
// holding abortMu, identPending stays empty
|
||||
c.wg.Wait()
|
||||
c.identMu.Lock()
|
||||
c.toplevel.Store(newToplevel(c.parent))
|
||||
clear(c.identErr)
|
||||
c.identMu.Unlock()
|
||||
}
|
||||
|
||||
// Close cancels all pending cures and waits for them to clean up.
|
||||
func (c *Cache) Close() {
|
||||
c.closeOnce.Do(func() {
|
||||
c.cancel()
|
||||
c.closeMu.Lock()
|
||||
defer c.closeMu.Unlock()
|
||||
|
||||
if c.closed {
|
||||
return
|
||||
}
|
||||
|
||||
c.closed = true
|
||||
c.toplevel.Load().cancel()
|
||||
c.wg.Wait()
|
||||
close(c.cures)
|
||||
c.unlock()
|
||||
})
|
||||
}
|
||||
|
||||
// UnsupportedVariantError describes an on-disk cache with an extension variant
|
||||
// identification string that differs from the value returned by [Extension].
|
||||
type UnsupportedVariantError string
|
||||
|
||||
func (e UnsupportedVariantError) Error() string {
|
||||
return "unsupported variant " + strconv.Quote(string(e))
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrWouldPromote is returned by [Open] if the [CPromoteVariant] bit is not
|
||||
// set and the on-disk cache requires variant promotion.
|
||||
ErrWouldPromote = errors.New("operation would promote unextended cache")
|
||||
)
|
||||
|
||||
// Open returns the address of a newly opened instance of [Cache].
|
||||
//
|
||||
// Concurrent cures of a [FloodArtifact] dependency graph is limited to the
|
||||
// caller-supplied value, however direct calls to [Cache.Cure] is not subject
|
||||
// to this limitation.
|
||||
//
|
||||
// A cures value of 0 or lower is equivalent to the value returned by
|
||||
// A cures or jobs value of 0 or lower is equivalent to the value returned by
|
||||
// [runtime.NumCPU].
|
||||
//
|
||||
// A successful call to Open guarantees exclusive access to the on-filesystem
|
||||
@@ -1820,10 +2041,10 @@ func (c *Cache) Close() {
|
||||
func Open(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
flags, cures int,
|
||||
flags, cures, jobs int,
|
||||
base *check.Absolute,
|
||||
) (*Cache, error) {
|
||||
return open(ctx, msg, flags, cures, base, true)
|
||||
return open(ctx, msg, flags, cures, jobs, base, true)
|
||||
}
|
||||
|
||||
// open implements Open but allows omitting the [lockedfile] lock when called
|
||||
@@ -1831,13 +2052,24 @@ func Open(
|
||||
func open(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
flags, cures int,
|
||||
flags, cures, jobs int,
|
||||
base *check.Absolute,
|
||||
lock bool,
|
||||
) (*Cache, error) {
|
||||
openMu.Lock()
|
||||
defer openMu.Unlock()
|
||||
opened = true
|
||||
|
||||
if extension == "" && len(irArtifact) != int(_kindEnd) {
|
||||
panic("attempting to open cache with incomplete variant setup")
|
||||
}
|
||||
|
||||
if cures < 1 {
|
||||
cures = runtime.NumCPU()
|
||||
}
|
||||
if jobs < 1 {
|
||||
jobs = runtime.NumCPU()
|
||||
}
|
||||
|
||||
for _, name := range []string{
|
||||
dirIdentifier,
|
||||
@@ -1845,29 +2077,34 @@ func open(
|
||||
dirStatus,
|
||||
dirWork,
|
||||
} {
|
||||
if err := os.MkdirAll(base.Append(name).String(), 0700); err != nil &&
|
||||
!errors.Is(err, os.ErrExist) {
|
||||
if err := os.MkdirAll(
|
||||
base.Append(name).String(),
|
||||
0700,
|
||||
); err != nil && !errors.Is(err, os.ErrExist) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c := Cache{
|
||||
parent: ctx,
|
||||
|
||||
cures: make(chan struct{}, cures),
|
||||
flags: flags,
|
||||
jobs: jobs,
|
||||
|
||||
msg: msg,
|
||||
base: base,
|
||||
|
||||
identPool: sync.Pool{New: func() any { return new(extIdent) }},
|
||||
irCache: zeroIRCache(),
|
||||
|
||||
ident: make(map[unique.Handle[ID]]unique.Handle[Checksum]),
|
||||
identErr: make(map[unique.Handle[ID]]error),
|
||||
identPending: make(map[unique.Handle[ID]]<-chan struct{}),
|
||||
identPending: make(map[unique.Handle[ID]]*pendingCure),
|
||||
|
||||
brPool: sync.Pool{New: func() any { return new(bufio.Reader) }},
|
||||
bwPool: sync.Pool{New: func() any { return new(bufio.Writer) }},
|
||||
}
|
||||
c.ctx, c.cancel = context.WithCancel(ctx)
|
||||
c.toplevel.Store(newToplevel(ctx))
|
||||
|
||||
if lock || !testing.Testing() {
|
||||
if unlock, err := lockedfile.MutexAt(
|
||||
@@ -1881,6 +2118,45 @@ func open(
|
||||
c.unlock = func() {}
|
||||
}
|
||||
|
||||
variantPath := base.Append(fileVariant).String()
|
||||
if p, err := os.ReadFile(variantPath); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
c.unlock()
|
||||
return nil, err
|
||||
}
|
||||
// nonexistence implies newly created cache, or a cache predating
|
||||
// variant identification strings, in which case it is silently promoted
|
||||
if err = os.WriteFile(
|
||||
variantPath,
|
||||
[]byte(extension),
|
||||
0400,
|
||||
); err != nil {
|
||||
c.unlock()
|
||||
return nil, err
|
||||
}
|
||||
} else if s := string(p); s == "" {
|
||||
if extension != "" {
|
||||
if flags&CPromoteVariant == 0 {
|
||||
c.unlock()
|
||||
return nil, ErrWouldPromote
|
||||
}
|
||||
if err = os.WriteFile(
|
||||
variantPath,
|
||||
[]byte(extension),
|
||||
0400,
|
||||
); err != nil {
|
||||
c.unlock()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else if !ValidExtension(s) {
|
||||
c.unlock()
|
||||
return nil, ErrInvalidExtension
|
||||
} else if s != extension {
|
||||
c.unlock()
|
||||
return nil, UnsupportedVariantError(s)
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
|
||||
+598
-98
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user