diff --git a/test/sandbox/fs.go b/test/sandbox/fs.go new file mode 100644 index 0000000..fd1aa2c --- /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 0000000..1262dda --- /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) + } + }) + } +}