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 { return p.mountinfo(func(d *vfs.MountInfoDecoder) error {
n, err := d.Unfold(targetKFinal) n, err := d.Unfold(targetKFinal)
if err != nil { if err != nil {
if errors.Is(err, ESTALE) { if errors.As(err, new(vfs.UnfoldTargetError)) {
return msg.WrapErr(err, return msg.WrapErr(err,
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal)) 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}, {"close", expectArgs{0xdeadbeef}, nil, errUnique},
}}, 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) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil}, {"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}, {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil}, {"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), 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 { {"mountinfo", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) 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}, {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil}, {"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), 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 { {"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) 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) 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) { 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) t.Fatalf("mountinfo: error = %v, want %v", err, wantErr)
} }

View File

@ -24,6 +24,32 @@ var (
ErrMountInfoSep = errors.New("bad optional fields separator") 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 ( type (
// A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream. // A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream.
MountInfoDecoder struct { MountInfoDecoder struct {
@ -32,6 +58,7 @@ type (
current *MountInfo current *MountInfo
parseErr error parseErr error
curLine int
complete bool complete bool
} }
@ -132,9 +159,12 @@ func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] {
func (d *MountInfoDecoder) Err() error { func (d *MountInfoDecoder) Err() error {
if err := d.s.Err(); err != nil { 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 { func (d *MountInfoDecoder) scan() bool {
@ -160,6 +190,7 @@ func (d *MountInfoDecoder) scan() bool {
d.current.Next = m d.current.Next = m
d.current = d.current.Next d.current = d.current.Next
} }
d.curLine++
return true return true
} }

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"iter" "iter"
"os"
"path" "path"
"reflect" "reflect"
"slices" "slices"
@ -15,62 +16,102 @@ import (
"hakurei.app/container/vfs" "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) { func TestMountInfo(t *testing.T) {
testCases := []mountInfoTest{ testCases := []mountInfoTest{
{"count", sampleMountinfoBase + ` {"count", sampleMountinfoBase + `
21 20 0:53/ /mnt/test rw,relatime - tmpfs rw 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`, 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 + ` {"sep", sampleMountinfoBase + `
21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw 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`, 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", sampleMountinfoBase + `
id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 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`, 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 + ` {"parent", sampleMountinfoBase + `
21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 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`, 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 + ` {"devno", sampleMountinfoBase + `
21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw 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`, 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 + ` {"maj", sampleMountinfoBase + `
21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 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`, 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 + ` {"min", sampleMountinfoBase + `
21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw 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`, 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 + ` {"mountroot", sampleMountinfoBase + `
21 20 0:53 /mnt/test rw,relatime - tmpfs rw 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`, 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 + ` {"target", sampleMountinfoBase + `
21 20 0:53 / rw,relatime - tmpfs rw 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`, 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 + ` {"vfs options", sampleMountinfoBase + `
21 20 0:53 / /mnt/test - tmpfs rw 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`, 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 + ` {"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
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.ErrMountInfoEmpty, "", nil, nil, nil}, &vfs.DecoderError{Op: "parse", Line: 7, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
{"base", sampleMountinfoBase, nil, "", []*wantMountInfo{ {"base", sampleMountinfoBase, nil, "", []*wantMountInfo{
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil), 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 { } else if tc.wantNode != nil || tc.wantCollectF != nil {
panic("invalid test case") 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 == "" { if tc.wantError == "" {
t.Errorf("Unfold: error = %v, wantErr %v", t.Errorf("Unfold: error = %#v, wantErr %#v",
err, tc.wantErr) err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError { } else if err != nil && err.Error() != tc.wantError {
t.Errorf("Unfold: error = %q, wantError %q", 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 == "" { if tc.wantError == "" {
t.Errorf("%s: error = %v, wantErr %v", t.Errorf("%s: error = %#v, wantErr %#v",
funcName, err, tc.wantErr) funcName, err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError { } else if err != nil && err.Error() != tc.wantError {
t.Errorf("%s: error = %q, wantError %q", t.Errorf("%s: error = %q, wantError %q",

View File

@ -4,9 +4,14 @@ import (
"iter" "iter"
"path" "path"
"strings" "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. // MountInfoNode positions a [MountInfoEntry] in its mount hierarchy.
type MountInfoNode struct { type MountInfoNode struct {
*MountInfoEntry *MountInfoEntry
@ -65,7 +70,8 @@ func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) {
} }
if targetIndex == -1 { 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 { for _, cur := range mountinfo {

View File

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