container/init: op interface valid method
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m12s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m25s

Check ops early and eliminate duplicate checks.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-08-21 00:18:50 +09:00
parent a1482ecdd0
commit 5d8a2199b6
Signed by: cat
SSH Key Fingerprint: SHA256:wr6yH7sDDbUFi81k/GsIGwpM3O2QrwqYlLF26CcJa4w
24 changed files with 162 additions and 76 deletions

View File

@ -21,6 +21,7 @@ func (f *Ops) Etc(host *Absolute, prefix string) *Ops {
type AutoEtcOp struct{ Prefix string } type AutoEtcOp struct{ Prefix string }
func (e *AutoEtcOp) Valid() bool { return e != nil }
func (e *AutoEtcOp) early(*setupState) error { return nil } func (e *AutoEtcOp) early(*setupState) error { return nil }
func (e *AutoEtcOp) apply(state *setupState) error { func (e *AutoEtcOp) apply(state *setupState) error {
if state.nonrepeatable&nrAutoEtc != 0 { if state.nonrepeatable&nrAutoEtc != 0 {
@ -66,7 +67,7 @@ func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtcOp) Is(op Op) bool { func (e *AutoEtcOp) Is(op Op) bool {
ve, ok := op.(*AutoEtcOp) ve, ok := op.(*AutoEtcOp)
return ok && ((e == nil && ve == nil) || (e != nil && ve != nil && *e == *ve)) return ok && e.Valid() && ve.Valid() && *e == *ve
} }
func (*AutoEtcOp) prefix() string { return "setting up" } func (*AutoEtcOp) prefix() string { return "setting up" }
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) } func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }

View File

@ -3,6 +3,12 @@ package container
import "testing" import "testing"
func TestAutoEtcOp(t *testing.T) { func TestAutoEtcOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*AutoEtcOp)(nil), false},
{"zero", new(AutoEtcOp), true},
{"populated", &AutoEtcOp{Prefix: ":3"}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"pd", new(Ops).Etc(MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{ {"pd", new(Ops).Etc(MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
&MkdirOp{Path: MustAbs("/etc/"), Perm: 0755}, &MkdirOp{Path: MustAbs("/etc/"), Perm: 0755},

View File

@ -28,11 +28,9 @@ type AutoRootOp struct {
resolved []Op resolved []Op
} }
func (r *AutoRootOp) early(state *setupState) error { func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil }
if r.Host == nil {
return syscall.EBADE
}
func (r *AutoRootOp) early(state *setupState) error {
if d, err := os.ReadDir(r.Host.String()); err != nil { if d, err := os.ReadDir(r.Host.String()); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} else { } else {
@ -72,9 +70,10 @@ func (r *AutoRootOp) apply(state *setupState) error {
func (r *AutoRootOp) Is(op Op) bool { func (r *AutoRootOp) Is(op Op) bool {
vr, ok := op.(*AutoRootOp) vr, ok := op.(*AutoRootOp)
return ok && ((r == nil && vr == nil) || (r != nil && vr != nil && return ok && r.Valid() && vr.Valid() &&
r.Host != nil && vr.Host != nil && r.Host.Is(vr.Host) && r.Host.Is(vr.Host) &&
r.Prefix == vr.Prefix && r.Flags == vr.Flags)) r.Prefix == vr.Prefix &&
r.Flags == vr.Flags
} }
func (*AutoRootOp) prefix() string { return "setting up" } func (*AutoRootOp) prefix() string { return "setting up" }
func (r *AutoRootOp) String() string { func (r *AutoRootOp) String() string {

View File

@ -3,6 +3,12 @@ package container
import "testing" import "testing"
func TestAutoRootOp(t *testing.T) { func TestAutoRootOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*AutoRootOp)(nil), false},
{"zero", new(AutoRootOp), false},
{"valid", &AutoRootOp{Host: MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"pd", new(Ops).Root(MustAbs("/"), "048090b6ed8f9ebb10e275ff5d8c0659", BindWritable), Ops{ {"pd", new(Ops).Root(MustAbs("/"), "048090b6ed8f9ebb10e275ff5d8c0659", BindWritable), Ops{
&AutoRootOp{ &AutoRootOp{

View File

@ -52,6 +52,7 @@ type (
prefix() string prefix() string
Is(op Op) bool Is(op Op) bool
Valid() bool
fmt.Stringer fmt.Stringer
} }
@ -167,8 +168,8 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
via library functions after pivot_root, and implementations are expected to avoid changing via library functions after pivot_root, and implementations are expected to avoid changing
the state of the mount namespace */ the state of the mount namespace */
for i, op := range *params.Ops { for i, op := range *params.Ops {
if op == nil { if op == nil || !op.Valid() {
log.Fatalf("invalid op %d", i) log.Fatalf("invalid op at index %d", i)
} }
if err := op.early(state); err != nil { if err := op.early(state); err != nil {

View File

@ -24,6 +24,8 @@ type BindMountOp struct {
Flags int Flags int
} }
func (b *BindMountOp) Valid() bool { return b != nil && b.Source != nil && b.Target != nil }
const ( const (
// BindOptional skips nonexistent host paths. // BindOptional skips nonexistent host paths.
BindOptional = 1 << iota BindOptional = 1 << iota
@ -34,10 +36,6 @@ const (
) )
func (b *BindMountOp) early(*setupState) error { func (b *BindMountOp) early(*setupState) error {
if b.Source == nil || b.Target == nil {
return EBADE
}
if pathname, err := filepath.EvalSymlinks(b.Source.String()); err != nil { if pathname, err := filepath.EvalSymlinks(b.Source.String()); err != nil {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 { if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
// leave sourceFinal as nil // leave sourceFinal as nil
@ -87,10 +85,10 @@ func (b *BindMountOp) apply(*setupState) error {
func (b *BindMountOp) Is(op Op) bool { func (b *BindMountOp) Is(op Op) bool {
vb, ok := op.(*BindMountOp) vb, ok := op.(*BindMountOp)
return ok && ((b == nil && vb == nil) || (b != nil && vb != nil && return ok && b.Valid() && vb.Valid() &&
b.Source != nil && vb.Source != nil && b.Source.Is(vb.Source) && b.Source.Is(vb.Source) &&
b.Target != nil && vb.Target != nil && b.Target.Is(vb.Target) && b.Target.Is(vb.Target) &&
b.Flags == vb.Flags)) b.Flags == vb.Flags
} }
func (*BindMountOp) prefix() string { return "mounting" } func (*BindMountOp) prefix() string { return "mounting" }
func (b *BindMountOp) String() string { func (b *BindMountOp) String() string {

View File

@ -3,6 +3,14 @@ package container
import "testing" import "testing"
func TestBindMountOp(t *testing.T) { func TestBindMountOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*BindMountOp)(nil), false},
{"zero", new(BindMountOp), false},
{"nil source", &BindMountOp{Target: MustAbs("/")}, false},
{"nil target", &BindMountOp{Source: MustAbs("/")}, false},
{"valid", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"autoetc", new(Ops).Bind( {"autoetc", new(Ops).Bind(
MustAbs("/etc/"), MustAbs("/etc/"),

View File

@ -33,11 +33,9 @@ type MountDevOp struct {
Write bool Write bool
} }
func (d *MountDevOp) Valid() bool { return d != nil && d.Target != nil }
func (d *MountDevOp) early(*setupState) error { return nil } func (d *MountDevOp) early(*setupState) error { return nil }
func (d *MountDevOp) apply(state *setupState) error { func (d *MountDevOp) apply(state *setupState) error {
if d.Target == nil {
return EBADE
}
target := toSysroot(d.Target.String()) target := toSysroot(d.Target.String())
if err := mountTmpfs(SourceTmpfsDevtmpfs, target, MS_NOSUID|MS_NODEV, 0, state.ParentPerm); err != nil { if err := mountTmpfs(SourceTmpfsDevtmpfs, target, MS_NOSUID|MS_NODEV, 0, state.ParentPerm); err != nil {
@ -128,9 +126,10 @@ func (d *MountDevOp) apply(state *setupState) error {
func (d *MountDevOp) Is(op Op) bool { func (d *MountDevOp) Is(op Op) bool {
vd, ok := op.(*MountDevOp) vd, ok := op.(*MountDevOp)
return ok && ((d == nil && vd == nil) || (d != nil && vd != nil && return ok && d.Valid() && vd.Valid() &&
d.Target != nil && vd.Target != nil && d.Target.Is(vd.Target) && d.Target.Is(vd.Target) &&
d.Mqueue == vd.Mqueue && d.Write == vd.Write)) d.Mqueue == vd.Mqueue &&
d.Write == vd.Write
} }
func (*MountDevOp) prefix() string { return "mounting" } func (*MountDevOp) prefix() string { return "mounting" }
func (d *MountDevOp) String() string { func (d *MountDevOp) String() string {

View File

@ -3,6 +3,12 @@ package container
import "testing" import "testing"
func TestMountDevOp(t *testing.T) { func TestMountDevOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountDevOp)(nil), false},
{"zero", new(MountDevOp), false},
{"valid", &MountDevOp{Target: MustAbs("/dev/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"dev", new(Ops).Dev(MustAbs("/dev/"), true), Ops{ {"dev", new(Ops).Dev(MustAbs("/dev/"), true), Ops{
&MountDevOp{ &MountDevOp{

View File

@ -4,7 +4,6 @@ import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"os" "os"
"syscall"
) )
func init() { gob.Register(new(MkdirOp)) } func init() { gob.Register(new(MkdirOp)) }
@ -21,19 +20,17 @@ type MkdirOp struct {
Perm os.FileMode Perm os.FileMode
} }
func (m *MkdirOp) Valid() bool { return m != nil && m.Path != nil }
func (m *MkdirOp) early(*setupState) error { return nil } func (m *MkdirOp) early(*setupState) error { return nil }
func (m *MkdirOp) apply(*setupState) error { func (m *MkdirOp) apply(*setupState) error {
if m.Path == nil {
return syscall.EBADE
}
return wrapErrSelf(os.MkdirAll(toSysroot(m.Path.String()), m.Perm)) return wrapErrSelf(os.MkdirAll(toSysroot(m.Path.String()), m.Perm))
} }
func (m *MkdirOp) Is(op Op) bool { func (m *MkdirOp) Is(op Op) bool {
vm, ok := op.(*MkdirOp) vm, ok := op.(*MkdirOp)
return ok && ((m == nil && vm == nil) || (m != nil && vm != nil && return ok && m.Valid() && vm.Valid() &&
m.Path != nil && vm.Path != nil && m.Path.Is(vm.Path) && m.Path.Is(vm.Path) &&
m.Perm == vm.Perm)) m.Perm == vm.Perm
} }
func (*MkdirOp) prefix() string { return "creating" } func (*MkdirOp) prefix() string { return "creating" }
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) } func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }

View File

@ -3,6 +3,12 @@ package container
import "testing" import "testing"
func TestMkdirOp(t *testing.T) { func TestMkdirOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*MkdirOp)(nil), false},
{"zero", new(MkdirOp), false},
{"valid", &MkdirOp{Path: MustAbs("/.hakurei")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"etc", new(Ops).Mkdir(MustAbs("/etc/"), 0), Ops{ {"etc", new(Ops).Mkdir(MustAbs("/etc/"), 0), Ops{
&MkdirOp{Path: MustAbs("/etc/")}, &MkdirOp{Path: MustAbs("/etc/")},

View File

@ -53,10 +53,10 @@ type MountOverlayOp struct {
lower []string lower []string
// The upperdir is normally on a writable filesystem. // The upperdir is normally on a writable filesystem.
// //
// If Work is nil and Upper holds the special value [FHSRoot], // If Work is nil and Upper holds the special value [AbsFHSRoot],
// an ephemeral upperdir and workdir will be set up. // an ephemeral upperdir and workdir will be set up.
// //
// If both Work and Upper are empty strings, upperdir and workdir is omitted and the overlay is mounted readonly. // If both Work and Upper are nil, upperdir and workdir is omitted and the overlay is mounted readonly.
Upper *Absolute Upper *Absolute
// formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early // formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early
upper string upper string
@ -68,6 +68,19 @@ type MountOverlayOp struct {
ephemeral bool ephemeral bool
} }
func (o *MountOverlayOp) Valid() bool {
if o == nil {
return false
}
if o.Work != nil && o.Upper == nil {
return false
}
if slices.Contains(o.Lower, nil) {
return false
}
return o.Target != nil
}
func (o *MountOverlayOp) early(*setupState) error { func (o *MountOverlayOp) early(*setupState) error {
if o.Work == nil && o.Upper != nil { if o.Work == nil && o.Upper != nil {
switch o.Upper.String() { switch o.Upper.String() {
@ -104,11 +117,7 @@ func (o *MountOverlayOp) early(*setupState) error {
} }
o.lower = make([]string, len(o.Lower)) o.lower = make([]string, len(o.Lower))
for i, a := range o.Lower { for i, a := range o.Lower { // nil checked in Valid
if a == nil {
return EBADE
}
if v, err := filepath.EvalSymlinks(a.String()); err != nil { if v, err := filepath.EvalSymlinks(a.String()); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} else { } else {
@ -119,9 +128,6 @@ func (o *MountOverlayOp) early(*setupState) error {
} }
func (o *MountOverlayOp) apply(state *setupState) error { func (o *MountOverlayOp) apply(state *setupState) error {
if o.Target == nil {
return EBADE
}
target := toSysroot(o.Target.String()) target := toSysroot(o.Target.String())
if err := os.MkdirAll(target, state.ParentPerm); err != nil { if err := os.MkdirAll(target, state.ParentPerm); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
@ -163,10 +169,10 @@ func (o *MountOverlayOp) apply(state *setupState) error {
func (o *MountOverlayOp) Is(op Op) bool { func (o *MountOverlayOp) Is(op Op) bool {
vo, ok := op.(*MountOverlayOp) vo, ok := op.(*MountOverlayOp)
return ok && ((o == nil && vo == nil) || (o != nil && vo != nil && return ok && o.Valid() && vo.Valid() &&
o.Target != nil && vo.Target != nil && o.Target.Is(vo.Target) && o.Target.Is(vo.Target) &&
slices.EqualFunc(o.Lower, vo.Lower, func(a *Absolute, v *Absolute) bool { return a.Is(v) }) && slices.EqualFunc(o.Lower, vo.Lower, func(a *Absolute, v *Absolute) bool { return a.Is(v) }) &&
o.Upper.Is(vo.Upper) && o.Work.Is(vo.Work))) o.Upper.Is(vo.Upper) && o.Work.Is(vo.Work)
} }
func (*MountOverlayOp) prefix() string { return "mounting" } func (*MountOverlayOp) prefix() string { return "mounting" }
func (o *MountOverlayOp) String() string { func (o *MountOverlayOp) String() string {

View File

@ -3,6 +3,15 @@ package container
import "testing" import "testing"
func TestMountOverlayOp(t *testing.T) { func TestMountOverlayOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountOverlayOp)(nil), false},
{"zero", new(MountOverlayOp), false},
{"nil lower", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{nil}}, false},
{"ro", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{MustAbs("/")}}, true},
{"ro work", &MountOverlayOp{Target: MustAbs("/"), Work: MustAbs("/tmp/")}, false},
{"rw", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{MustAbs("/")}, Upper: MustAbs("/"), Work: MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"full", new(Ops).Overlay( {"full", new(Ops).Overlay(
MustAbs("/nix/store"), MustAbs("/nix/store"),

View File

@ -35,12 +35,9 @@ type TmpfileOp struct {
Data []byte Data []byte
} }
func (t *TmpfileOp) Valid() bool { return t != nil && t.Path != nil }
func (t *TmpfileOp) early(*setupState) error { return nil } func (t *TmpfileOp) early(*setupState) error { return nil }
func (t *TmpfileOp) apply(state *setupState) error { func (t *TmpfileOp) apply(state *setupState) error {
if t.Path == nil {
return EBADE
}
var tmpPath string var tmpPath string
if f, err := os.CreateTemp(FHSRoot, intermediatePatternTmpfile); err != nil { if f, err := os.CreateTemp(FHSRoot, intermediatePatternTmpfile); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
@ -72,7 +69,8 @@ func (t *TmpfileOp) apply(state *setupState) error {
func (t *TmpfileOp) Is(op Op) bool { func (t *TmpfileOp) Is(op Op) bool {
vt, ok := op.(*TmpfileOp) vt, ok := op.(*TmpfileOp)
return ok && t.Path != nil && vt.Path != nil && t.Path.Is(vt.Path) && return ok && t.Valid() && vt.Valid() &&
t.Path.Is(vt.Path) &&
string(t.Data) == string(vt.Data) string(t.Data) == string(vt.Data)
} }
func (*TmpfileOp) prefix() string { return "placing" } func (*TmpfileOp) prefix() string { return "placing" }

View File

@ -3,6 +3,12 @@ package container
import "testing" import "testing"
func TestTmpfileOp(t *testing.T) { func TestTmpfileOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*TmpfileOp)(nil), false},
{"zero", new(TmpfileOp), false},
{"valid", &TmpfileOp{Path: MustAbs("/etc/passwd")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"noref", new(Ops).Place(MustAbs("/etc/passwd"), []byte(`chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`)), Ops{ {"noref", new(Ops).Place(MustAbs("/etc/passwd"), []byte(`chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`)), Ops{
&TmpfileOp{ &TmpfileOp{

View File

@ -16,15 +16,11 @@ func (f *Ops) Proc(target *Absolute) *Ops {
} }
// MountProcOp mounts a new instance of [FstypeProc] on container path Target. // MountProcOp mounts a new instance of [FstypeProc] on container path Target.
type MountProcOp struct { type MountProcOp struct{ Target *Absolute }
Target *Absolute
}
func (p *MountProcOp) Valid() bool { return p != nil && p.Target != nil }
func (p *MountProcOp) early(*setupState) error { return nil } func (p *MountProcOp) early(*setupState) error { return nil }
func (p *MountProcOp) apply(state *setupState) error { func (p *MountProcOp) apply(state *setupState) error {
if p.Target == nil {
return EBADE
}
target := toSysroot(p.Target.String()) target := toSysroot(p.Target.String())
if err := os.MkdirAll(target, state.ParentPerm); err != nil { if err := os.MkdirAll(target, state.ParentPerm); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
@ -35,8 +31,8 @@ func (p *MountProcOp) apply(state *setupState) error {
func (p *MountProcOp) Is(op Op) bool { func (p *MountProcOp) Is(op Op) bool {
vp, ok := op.(*MountProcOp) vp, ok := op.(*MountProcOp)
return ok && ((p == nil && vp == nil) || return ok && p.Valid() && vp.Valid() &&
(p.Target != nil && vp.Target != nil && p.Target.Is(vp.Target))) p.Target.Is(vp.Target)
} }
func (*MountProcOp) prefix() string { return "mounting" } func (*MountProcOp) prefix() string { return "mounting" }
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) } func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }

View File

@ -3,6 +3,12 @@ package container
import "testing" import "testing"
func TestMountProcOp(t *testing.T) { func TestMountProcOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountProcOp)(nil), false},
{"zero", new(MountProcOp), false},
{"valid", &MountProcOp{Target: MustAbs("/proc/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"proc", new(Ops).Proc(MustAbs("/proc/")), Ops{ {"proc", new(Ops).Proc(MustAbs("/proc/")), Ops{
&MountProcOp{Target: MustAbs("/proc/")}, &MountProcOp{Target: MustAbs("/proc/")},

View File

@ -3,7 +3,6 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"syscall"
) )
func init() { gob.Register(new(RemountOp)) } func init() { gob.Register(new(RemountOp)) }
@ -20,20 +19,18 @@ type RemountOp struct {
Flags uintptr Flags uintptr
} }
func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil }
func (*RemountOp) early(*setupState) error { return nil } func (*RemountOp) early(*setupState) error { return nil }
func (r *RemountOp) apply(*setupState) error { func (r *RemountOp) apply(*setupState) error {
if r.Target == nil {
return syscall.EBADE
}
return wrapErrSuffix(hostProc.remount(toSysroot(r.Target.String()), r.Flags), return wrapErrSuffix(hostProc.remount(toSysroot(r.Target.String()), r.Flags),
fmt.Sprintf("cannot remount %q:", r.Target)) fmt.Sprintf("cannot remount %q:", r.Target))
} }
func (r *RemountOp) Is(op Op) bool { func (r *RemountOp) Is(op Op) bool {
vr, ok := op.(*RemountOp) vr, ok := op.(*RemountOp)
return ok && ((r == nil && vr == nil) || return ok && r.Valid() && vr.Valid() &&
(r.Target != nil && vr.Target != nil && r.Target.Is(vr.Target)) && r.Target.Is(vr.Target) &&
r.Flags == vr.Flags) r.Flags == vr.Flags
} }
func (*RemountOp) prefix() string { return "remounting" } func (*RemountOp) prefix() string { return "remounting" }
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) } func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }

View File

@ -6,6 +6,12 @@ import (
) )
func TestRemountOp(t *testing.T) { func TestRemountOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*RemountOp)(nil), false},
{"zero", new(RemountOp), false},
{"valid", &RemountOp{Target: MustAbs("/"), Flags: syscall.MS_RDONLY}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"root", new(Ops).Remount(MustAbs("/"), syscall.MS_RDONLY), Ops{ {"root", new(Ops).Remount(MustAbs("/"), syscall.MS_RDONLY), Ops{
&RemountOp{ &RemountOp{

View File

@ -26,6 +26,8 @@ type SymlinkOp struct {
Dereference bool Dereference bool
} }
func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkName != zeroString }
func (l *SymlinkOp) early(*setupState) error { func (l *SymlinkOp) early(*setupState) error {
if l.Dereference { if l.Dereference {
if !isAbs(l.LinkName) { if !isAbs(l.LinkName) {
@ -41,9 +43,6 @@ func (l *SymlinkOp) early(*setupState) error {
} }
func (l *SymlinkOp) apply(state *setupState) error { func (l *SymlinkOp) apply(state *setupState) error {
if l.Target == nil {
return syscall.EBADE
}
target := toSysroot(l.Target.String()) target := toSysroot(l.Target.String())
if err := os.MkdirAll(path.Dir(target), state.ParentPerm); err != nil { if err := os.MkdirAll(path.Dir(target), state.ParentPerm); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
@ -56,9 +55,10 @@ func (l *SymlinkOp) apply(state *setupState) error {
func (l *SymlinkOp) Is(op Op) bool { func (l *SymlinkOp) Is(op Op) bool {
vl, ok := op.(*SymlinkOp) vl, ok := op.(*SymlinkOp)
return ok && ((l == nil && vl == nil) || return ok && l.Valid() && vl.Valid() &&
(l.Target != nil && vl.Target != nil && l.Target.Is(vl.Target)) && l.Target.Is(vl.Target) &&
l.LinkName == vl.LinkName && l.Dereference == vl.Dereference) l.LinkName == vl.LinkName &&
l.Dereference == vl.Dereference
} }
func (*SymlinkOp) prefix() string { return "creating" } func (*SymlinkOp) prefix() string { return "creating" }
func (l *SymlinkOp) String() string { func (l *SymlinkOp) String() string {

View File

@ -3,6 +3,14 @@ package container
import "testing" import "testing"
func TestSymlinkOp(t *testing.T) { func TestSymlinkOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*SymlinkOp)(nil), false},
{"zero", new(SymlinkOp), false},
{"nil target", &SymlinkOp{LinkName: "/run/current-system"}, false},
{"zero linkname", &SymlinkOp{Target: MustAbs("/run/current-system")}, false},
{"valid", &SymlinkOp{Target: MustAbs("/run/current-system"), LinkName: "/run/current-system", Dereference: true}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"current-system", new(Ops).Link( {"current-system", new(Ops).Link(
MustAbs("/run/current-system"), MustAbs("/run/current-system"),

View File

@ -31,11 +31,9 @@ type MountTmpfsOp struct {
Perm os.FileMode Perm os.FileMode
} }
func (t *MountTmpfsOp) Valid() bool { return t != nil && t.Path != nil && t.FSName != zeroString }
func (t *MountTmpfsOp) early(*setupState) error { return nil } func (t *MountTmpfsOp) early(*setupState) error { return nil }
func (t *MountTmpfsOp) apply(*setupState) error { func (t *MountTmpfsOp) apply(*setupState) error {
if t.Path == nil {
return EBADE
}
if t.Size < 0 || t.Size > math.MaxUint>>1 { if t.Size < 0 || t.Size > math.MaxUint>>1 {
return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size)) return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size))
} }
@ -44,9 +42,12 @@ func (t *MountTmpfsOp) apply(*setupState) error {
func (t *MountTmpfsOp) Is(op Op) bool { func (t *MountTmpfsOp) Is(op Op) bool {
vt, ok := op.(*MountTmpfsOp) vt, ok := op.(*MountTmpfsOp)
return ok && ((t == nil && vt == nil) || return ok && t.Valid() && vt.Valid() &&
(t.Path != nil && vt.Path != nil && t.Path.Is(vt.Path)) && t.FSName == vt.FSName &&
t.FSName == vt.FSName && t.Flags == vt.Flags && t.Size == vt.Size && t.Perm == vt.Perm) t.Path.Is(vt.Path) &&
t.Flags == vt.Flags &&
t.Size == vt.Size &&
t.Perm == vt.Perm
} }
func (*MountTmpfsOp) prefix() string { return "mounting" } func (*MountTmpfsOp) prefix() string { return "mounting" }
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) } func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }

View File

@ -6,6 +6,14 @@ import (
) )
func TestMountTmpfsOp(t *testing.T) { func TestMountTmpfsOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountTmpfsOp)(nil), false},
{"zero", new(MountTmpfsOp), false},
{"nil path", &MountTmpfsOp{FSName: "tmpfs"}, false},
{"zero fsname", &MountTmpfsOp{Path: MustAbs("/tmp/")}, false},
{"valid", &MountTmpfsOp{FSName: "tmpfs", Path: MustAbs("/tmp/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"runtime", new(Ops).Tmpfs( {"runtime", new(Ops).Tmpfs(
MustAbs("/run/user"), MustAbs("/run/user"),

View File

@ -49,6 +49,24 @@ func TestEscapeOverlayDataSegment(t *testing.T) {
} }
} }
type opValidTestCase struct {
name string
op Op
want bool
}
func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
t.Run("valid", func(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.op.Valid(); got != tc.want {
t.Errorf("Valid: %v, want %v", got, tc.want)
}
})
}
})
}
type opsBuilderTestCase struct { type opsBuilderTestCase struct {
name string name string
ops *Ops ops *Ops