aboutsummaryrefslogtreecommitdiffhomepage
path: root/test/sandbox
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-03-02 22:52:21 +0900
committerOphestra <cat@gensokyo.uk>2025-03-02 22:59:04 +0900
commit39e32799b3ae889bfb05cead2708c82137eb795a (patch)
tree94b1c25c15ba4636a22a192bdbc7b783c82a277c /test/sandbox
parent9953768de5179ac0079721fbb33912519c645109 (diff)
test/sandbox: compare filesystem hierarchy
For checking deterministic aspects of fs outcome. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'test/sandbox')
-rw-r--r--test/sandbox/fs.go98
-rw-r--r--test/sandbox/fs_test.go78
2 files changed, 176 insertions, 0 deletions
diff --git a/test/sandbox/fs.go b/test/sandbox/fs.go
new file mode 100644
index 00000000..fd1aa2c5
--- /dev/null
+++ b/test/sandbox/fs.go
@@ -0,0 +1,98 @@
+package sandbox
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "path"
+ "strings"
+)
+
+var (
+ ErrFSBadLength = errors.New("bad dir length")
+ ErrFSBadData = errors.New("data differs")
+ ErrFSBadMode = errors.New("mode differs")
+ ErrFSInvalidEnt = errors.New("invalid entry condition")
+)
+
+type FS struct {
+ Mode fs.FileMode `json:"mode"`
+ Dir map[string]*FS `json:"dir"`
+ Data *string `json:"data"`
+}
+
+func printDir(prefix string, dir []fs.DirEntry) {
+ names := make([]string, len(dir))
+ for i, ent := range dir {
+ name := ent.Name()
+ if ent.IsDir() {
+ name += "/"
+ }
+ names[i] = fmt.Sprintf("%q", name)
+ }
+ printf("[FAIL] d %q: %s", prefix, strings.Join(names, " "))
+}
+
+func (s *FS) Compare(prefix string, e fs.FS) error {
+ if s.Data != nil {
+ if s.Dir != nil {
+ panic("invalid state")
+ }
+ panic("invalid compare call")
+ }
+
+ if s.Dir == nil {
+ printf("[ OK ] s %s", prefix)
+ return nil
+ }
+
+ var dir []fs.DirEntry
+ if d, err := fs.ReadDir(e, prefix); err != nil {
+ return err
+ } else if len(d) != len(s.Dir) {
+ printDir(prefix, d)
+ return ErrFSBadLength
+ } else {
+ dir = d
+ }
+
+ for _, got := range dir {
+ name := got.Name()
+
+ if want, ok := s.Dir[name]; !ok {
+ printDir(prefix, dir)
+ return fs.ErrNotExist
+ } else if want.Dir != nil && !got.IsDir() {
+ printDir(prefix, dir)
+ return ErrFSInvalidEnt
+ } else {
+ name = path.Join(prefix, name)
+
+ if fi, err := got.Info(); err != nil {
+ return err
+ } else if fi.Mode() != want.Mode {
+ printf("[FAIL] m %q: %x, want %x",
+ name, uint32(fi.Mode()), uint32(want.Mode))
+ return ErrFSBadMode
+ }
+
+ if want.Data != nil {
+ if want.Dir != nil {
+ panic("invalid state")
+ }
+ if v, err := fs.ReadFile(e, name); err != nil {
+ return err
+ } else if string(v) != *want.Data {
+ printf("[FAIL] f %s", name)
+ return ErrFSBadData
+ }
+ printf("[ OK ] f %s", name)
+ } else if err := want.Compare(name, e); err != nil {
+ return err
+ }
+ }
+ }
+ printf("[ OK ] d %s", prefix)
+
+ return nil
+}
diff --git a/test/sandbox/fs_test.go b/test/sandbox/fs_test.go
new file mode 100644
index 00000000..1262ddac
--- /dev/null
+++ b/test/sandbox/fs_test.go
@@ -0,0 +1,78 @@
+package sandbox_test
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "strings"
+ "testing"
+ "testing/fstest"
+
+ "git.gensokyo.uk/security/fortify/test/sandbox"
+)
+
+var (
+ fsPasswdSample = "u0_a20:x:65534:65534:Fortify:/var/lib/persist/module/fortify/u0/a20:/run/current-system/sw/bin/zsh"
+ fsGroupSample = "fortify:x:65534:"
+)
+
+func TestCompare(t *testing.T) {
+ testCases := []struct {
+ name string
+
+ sample fstest.MapFS
+ want *sandbox.FS
+ wantOut string
+ wantErr error
+ }{
+ {"skip", fstest.MapFS{}, &sandbox.FS{}, "[ OK ] s .\x00", nil},
+ {"simple pass", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
+ &sandbox.FS{Dir: map[string]*sandbox.FS{".fortify": {Mode: 0x800001ed}}},
+ "[ OK ] s .fortify\x00[ OK ] d .\x00", nil},
+ {"bad length", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
+ &sandbox.FS{Dir: make(map[string]*sandbox.FS)},
+ "[FAIL] d \".\": \".fortify/\"\x00", sandbox.ErrFSBadLength},
+ {"top level bad mode", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
+ &sandbox.FS{Dir: map[string]*sandbox.FS{".fortify": {Mode: 0xdeadbeef}}},
+ "[FAIL] m \".fortify\": 800001ed, want deadbeef\x00", sandbox.ErrFSBadMode},
+ {"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
+ &sandbox.FS{Dir: map[string]*sandbox.FS{"test": {Dir: make(map[string]*sandbox.FS)}}},
+ "[FAIL] d \".\": \"test\"\x00", sandbox.ErrFSInvalidEnt},
+ {"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
+ &sandbox.FS{Dir: map[string]*sandbox.FS{".test": {}}},
+ "[FAIL] d \".\": \"test\"\x00", fs.ErrNotExist},
+ {"file", fstest.MapFS{"etc": {Mode: 0x800001c0},
+ "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
+ "etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
+ }, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{
+ "passwd": {Mode: 0x1a4, Data: &fsPasswdSample},
+ "group": {Mode: 0x1a4, Data: &fsGroupSample},
+ }}}}, "[ OK ] f etc/group\x00[ OK ] f etc/passwd\x00[ OK ] d etc\x00[ OK ] d .\x00", nil},
+ {"file differ", fstest.MapFS{"etc": {Mode: 0x800001c0},
+ "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
+ "etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
+ }, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{
+ "passwd": {Mode: 0x1a4, Data: &fsGroupSample},
+ "group": {Mode: 0x1a4, Data: &fsGroupSample},
+ }}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\x00", sandbox.ErrFSBadData},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ gotOut := new(strings.Builder)
+ oldPrint := sandbox.SwapPrint(func(format string, v ...any) { _, _ = fmt.Fprintf(gotOut, format+"\x00", v...) })
+ t.Cleanup(func() { sandbox.SwapPrint(oldPrint) })
+
+ err := tc.want.Compare(".", tc.sample)
+ if !errors.Is(err, tc.wantErr) {
+ t.Errorf("Compare: error = %v; wantErr %v",
+ err, tc.wantErr)
+ }
+
+ if gotOut.String() != tc.wantOut {
+ t.Errorf("Compare: output %q; want %q",
+ gotOut, tc.wantOut)
+ }
+ })
+ }
+}