container/vfs: wrap decoder errors
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 3m33s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m35s

This passes line information and handles strconv errors so it reads better.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-08-29 21:51:31 +09:00
parent 905b9f9785
commit 50972096cd
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
7 changed files with 106 additions and 30 deletions

View File

@ -149,7 +149,7 @@ func (p *procPaths) remount(target string, flags uintptr) error {
return p.mountinfo(func(d *vfs.MountInfoDecoder) error {
n, err := d.Unfold(targetKFinal)
if err != nil {
if errors.Is(err, ESTALE) {
if errors.As(err, new(vfs.UnfoldTargetError)) {
return msg.WrapErr(err,
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
}

View File

@ -109,7 +109,7 @@ func TestRemount(t *testing.T) {
{"close", expectArgs{0xdeadbeef}, nil, errUnique},
}}, errUnique},
{"mountinfo stale", func(k syscallDispatcher) error {
{"mountinfo no match", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil},
@ -118,7 +118,7 @@ func TestRemount(t *testing.T) {
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
}}, msg.WrapErr(syscall.ESTALE, `mount point "/sysroot/.hakurei" never appeared in mountinfo`)},
}}, msg.WrapErr(&vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}, `mount point "/sysroot/.hakurei" never appeared in mountinfo`)},
{"mountinfo", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
@ -128,7 +128,7 @@ func TestRemount(t *testing.T) {
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil},
}}, wrapErrSuffix(vfs.ErrMountInfoFields, `cannot parse mountinfo:`)},
}}, wrapErrSuffix(&vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}, `cannot parse mountinfo:`)},
{"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)

View File

@ -248,7 +248,7 @@ func TestProcPaths(t *testing.T) {
t.Fatalf("WriteFile: error = %v", err)
}
wantErr := wrapErrSuffix(vfs.ErrMountInfoFields, "cannot parse mountinfo:")
wantErr := wrapErrSuffix(&vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}, "cannot parse mountinfo:")
if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !errors.Is(err, wantErr) {
t.Fatalf("mountinfo: error = %v, want %v", err, wantErr)
}

View File

@ -24,6 +24,32 @@ var (
ErrMountInfoSep = errors.New("bad optional fields separator")
)
type DecoderError struct {
Op string
Line int
Err error
}
func (e *DecoderError) Unwrap() error { return e.Err }
func (e *DecoderError) Error() string {
var s string
var numError *strconv.NumError
switch {
case errors.As(e.Err, &numError) && numError != nil:
s = "numeric field " + strconv.Quote(numError.Num) + " " + numError.Err.Error()
default:
s = e.Err.Error()
}
var atLine string
if e.Line >= 0 {
atLine = " at line " + strconv.Itoa(e.Line)
}
return e.Op + " mountinfo" + atLine + ": " + s
}
type (
// A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream.
MountInfoDecoder struct {
@ -32,6 +58,7 @@ type (
current *MountInfo
parseErr error
curLine int
complete bool
}
@ -132,9 +159,12 @@ func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] {
func (d *MountInfoDecoder) Err() error {
if err := d.s.Err(); err != nil {
return err
return &DecoderError{"scan", d.curLine, err}
}
return d.parseErr
if d.parseErr != nil {
return &DecoderError{"parse", d.curLine, d.parseErr}
}
return nil
}
func (d *MountInfoDecoder) scan() bool {
@ -160,6 +190,7 @@ func (d *MountInfoDecoder) scan() bool {
d.current.Next = m
d.current = d.current.Next
}
d.curLine++
return true
}

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"iter"
"os"
"path"
"reflect"
"slices"
@ -15,62 +16,102 @@ import (
"hakurei.app/container/vfs"
)
func TestDecoderError(t *testing.T) {
testCases := []struct {
name string
err *vfs.DecoderError
want string
target error
targetF error
}{
{"errno", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: syscall.ENOTRECOVERABLE},
"parse mountinfo at line 3735928559: state not recoverable", syscall.ENOTRECOVERABLE, syscall.EROFS},
{"strconv", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
`parse mountinfo at line 3735928559: numeric field "meow" invalid syntax`, strconv.ErrSyntax, os.ErrInvalid},
{"unfold", &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/proc/nonexistent")},
"unfold mountinfo: mount point /proc/nonexistent never appeared in mountinfo", vfs.UnfoldTargetError("/proc/nonexistent"), os.ErrNotExist},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %s, want %s", got, tc.want)
}
})
t.Run("is", func(t *testing.T) {
if !errors.Is(tc.err, tc.target) {
t.Errorf("Is: unexpected false")
}
if errors.Is(tc.err, tc.targetF) {
t.Errorf("Is: unexpected true")
}
})
})
}
}
func TestMountInfo(t *testing.T) {
testCases := []mountInfoTest{
{"count", sampleMountinfoBase + `
21 20 0:53/ /mnt/test rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoFields, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoFields},
"", nil, nil, nil},
{"sep", sampleMountinfoBase + `
21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoSep, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoSep},
"", nil, nil, nil},
{"id", sampleMountinfoBase + `
id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
strconv.ErrSyntax, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: &strconv.NumError{Func: "Atoi", Num: "id", Err: strconv.ErrSyntax}},
"", nil, nil, nil},
{"parent", sampleMountinfoBase + `
21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
strconv.ErrSyntax, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: &strconv.NumError{Func: "Atoi", Num: "parent", Err: strconv.ErrSyntax}}, "", nil, nil, nil},
{"devno", sampleMountinfoBase + `
21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "unexpected EOF", nil, nil, nil},
nil, "parse mountinfo at line 6: unexpected EOF", nil, nil, nil},
{"maj", sampleMountinfoBase + `
21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "expected integer", nil, nil, nil},
nil, "parse mountinfo at line 6: expected integer", nil, nil, nil},
{"min", sampleMountinfoBase + `
21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "expected integer", nil, nil, nil},
nil, "parse mountinfo at line 6: expected integer", nil, nil, nil},
{"mountroot", sampleMountinfoBase + `
21 20 0:53 /mnt/test rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
{"target", sampleMountinfoBase + `
21 20 0:53 / rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
{"vfs options", sampleMountinfoBase + `
21 20 0:53 / /mnt/test - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
{"FS type", sampleMountinfoBase + `
21 20 0:53 / /mnt/test rw,relatime - rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
21 20 0:53 / /mnt/test rw,relatime - rw`,
&vfs.DecoderError{Op: "parse", Line: 7, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
{"base", sampleMountinfoBase, nil, "", []*wantMountInfo{
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
@ -266,9 +307,9 @@ func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName s
})
} else if tc.wantNode != nil || tc.wantCollectF != nil {
panic("invalid test case")
} else if _, err := d.Unfold("/"); !errors.Is(err, tc.wantErr) {
} else if _, err := d.Unfold("/"); !reflect.DeepEqual(err, tc.wantErr) {
if tc.wantError == "" {
t.Errorf("Unfold: error = %v, wantErr %v",
t.Errorf("Unfold: error = %#v, wantErr %#v",
err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError {
t.Errorf("Unfold: error = %q, wantError %q",
@ -276,9 +317,9 @@ func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName s
}
}
if err := gotErr(); !errors.Is(err, tc.wantErr) {
if err := gotErr(); !reflect.DeepEqual(err, tc.wantErr) {
if tc.wantError == "" {
t.Errorf("%s: error = %v, wantErr %v",
t.Errorf("%s: error = %#v, wantErr %#v",
funcName, err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError {
t.Errorf("%s: error = %q, wantError %q",

View File

@ -4,9 +4,14 @@ import (
"iter"
"path"
"strings"
"syscall"
)
type UnfoldTargetError string
func (e UnfoldTargetError) Error() string {
return "mount point " + string(e) + " never appeared in mountinfo"
}
// MountInfoNode positions a [MountInfoEntry] in its mount hierarchy.
type MountInfoNode struct {
*MountInfoEntry
@ -65,7 +70,8 @@ func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) {
}
if targetIndex == -1 {
return nil, syscall.ESTALE
// target does not exist in parsed mountinfo
return nil, &DecoderError{Op: "unfold", Line: -1, Err: UnfoldTargetError(targetClean)}
}
for _, cur := range mountinfo {

View File

@ -1,11 +1,9 @@
package vfs_test
import (
"errors"
"reflect"
"slices"
"strings"
"syscall"
"testing"
"hakurei.app/container/vfs"
@ -26,7 +24,7 @@ func TestUnfold(t *testing.T) {
"no match",
sampleMountinfoBase,
"/mnt",
syscall.ESTALE, nil, nil, nil,
&vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/mnt")}, nil, nil, nil,
},
{
"cover",
@ -55,7 +53,7 @@ func TestUnfold(t *testing.T) {
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
got, err := d.Unfold(tc.target)
if !errors.Is(err, tc.wantErr) {
if !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("Unfold: error = %v, wantErr %v",
err, tc.wantErr)
}