diff options
Diffstat (limited to 'object/resolve')
| -rw-r--r-- | object/resolve/treefs.go | 30 | ||||
| -rw-r--r-- | object/resolve/treefs_entry.go | 82 | ||||
| -rw-r--r-- | object/resolve/treefs_info.go | 73 | ||||
| -rw-r--r-- | object/resolve/treefs_new.go | 17 | ||||
| -rw-r--r-- | object/resolve/treefs_op.go | 28 | ||||
| -rw-r--r-- | object/resolve/treefs_open.go | 121 | ||||
| -rw-r--r-- | object/resolve/treefs_path.go | 28 | ||||
| -rw-r--r-- | object/resolve/treefs_readdir.go | 19 | ||||
| -rw-r--r-- | object/resolve/treefs_readfile.go | 37 | ||||
| -rw-r--r-- | object/resolve/treefs_stat.go | 17 | ||||
| -rw-r--r-- | object/resolve/treefs_sub.go | 21 | ||||
| -rw-r--r-- | object/resolve/treefs_test.go | 109 |
12 files changed, 582 insertions, 0 deletions
diff --git a/object/resolve/treefs.go b/object/resolve/treefs.go new file mode 100644 index 00000000..eda8c7fa --- /dev/null +++ b/object/resolve/treefs.go @@ -0,0 +1,30 @@ +package resolve + +import ( + "io/fs" + + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/objectid" +) + +// TreeFS exposes one Git tree as an fs.FS. +// +// TreeFS interprets names using io/fs path rules. Those rules do not match raw +// Git tree entry naming exactly: names are UTF-8, slash-separated, and must be +// valid fs.FS paths. Tree entries that cannot be represented under those rules +// are not addressable through this API. +// +// TreeFS does not take ownership of its Resolver. +type TreeFS struct { + resolver *Resolver + rootTree objectid.ObjectID + rootEntry *object.TreeEntry +} + +var ( + _ fs.FS = (*TreeFS)(nil) + _ fs.ReadFileFS = (*TreeFS)(nil) + _ fs.ReadDirFS = (*TreeFS)(nil) + _ fs.StatFS = (*TreeFS)(nil) + _ fs.SubFS = (*TreeFS)(nil) +) diff --git a/object/resolve/treefs_entry.go b/object/resolve/treefs_entry.go new file mode 100644 index 00000000..402b47d2 --- /dev/null +++ b/object/resolve/treefs_entry.go @@ -0,0 +1,82 @@ +package resolve + +import ( + "fmt" + "io/fs" + "strings" + + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/objectid" +) + +func (treeFS *TreeFS) resolvePath(op treeFSOp, name string) (treeEntryValue, error) { + if !treeFSValidPath(name) { + return treeEntryValue{}, treeFSPathError(op, name, fs.ErrInvalid) + } + + if name == "." { + return treeEntryValue{ + name: ".", + mode: object.FileModeDir, + treeID: treeFS.rootTree, + treeEntry: treeFS.rootEntry, + }, nil + } + + entry, err := treeFS.resolver.Path(treeFS.rootTree, treeFSSplitPath(name)) + if err != nil { + return treeEntryValue{}, treeFS.pathResolveError(op, name, err) + } + + return treeEntryValue{ + name: string(entry.Name), + mode: entry.Mode, + objectID: entry.ID, + treeEntry: &entry, + }, nil +} + +func (treeFS *TreeFS) pathResolveError(op treeFSOp, name string, err error) error { + if err != nil && strings.Contains(err.Error(), "not found") { + return treeFSPathError(op, name, fs.ErrNotExist) + } + + if err != nil && strings.Contains(err.Error(), "is not a tree") { + return treeFSPathError(op, name, fs.ErrInvalid) + } + + return treeFSPathError(op, name, err) +} + +type treeEntryValue struct { + name string + mode object.FileMode + objectID objectid.ObjectID + treeID objectid.ObjectID + treeEntry *object.TreeEntry +} + +func (entry treeEntryValue) isDir() bool { + return entry.mode == object.FileModeDir +} + +func (entry treeEntryValue) blobSize(resolve *Resolver) (int64, error) { + _, size, err := resolve.store.ReadHeader(entry.objectID) + if err != nil { + return 0, err + } + + return size, nil +} + +func (entry treeEntryValue) subtreeID() (objectid.ObjectID, error) { + if entry.name == "." { + return entry.treeID, nil + } + + if entry.mode != object.FileModeDir { + return objectid.ObjectID{}, fmt.Errorf("object/resolve: path %q is not a tree", entry.name) + } + + return entry.objectID, nil +} diff --git a/object/resolve/treefs_info.go b/object/resolve/treefs_info.go new file mode 100644 index 00000000..3d441e91 --- /dev/null +++ b/object/resolve/treefs_info.go @@ -0,0 +1,73 @@ +package resolve + +import ( + "io/fs" + "time" + + "codeberg.org/lindenii/furgit/object" +) + +type treeFSInfo struct { + name string + mode fs.FileMode + size int64 + sys any + isDir bool +} + +var ( + _ fs.FileInfo = (*treeFSInfo)(nil) + _ fs.DirEntry = (*treeFSInfo)(nil) +) + +func (info *treeFSInfo) Name() string { return info.name } +func (info *treeFSInfo) Size() int64 { return info.size } +func (info *treeFSInfo) Mode() fs.FileMode { return info.mode } +func (info *treeFSInfo) Type() fs.FileMode { return info.mode.Type() } +func (info *treeFSInfo) IsDir() bool { return info.isDir } +func (info *treeFSInfo) ModTime() time.Time { return time.Time{} } +func (info *treeFSInfo) Sys() any { return info.sys } +func (info *treeFSInfo) Info() (fs.FileInfo, error) { + return info, nil +} + +func treeFSEntryMode(mode object.FileMode) fs.FileMode { + switch mode { + case object.FileModeDir: + return fs.ModeDir | 0o555 + case object.FileModeRegular: + return 0o444 + case object.FileModeExecutable: + return 0o555 + case object.FileModeSymlink: + return fs.ModeSymlink | 0o444 + case object.FileModeGitlink: + return fs.ModeIrregular + default: + return fs.ModeIrregular + } +} + +func (treeFS *TreeFS) statEntry(entry treeEntryValue) (*treeFSInfo, error) { + size := int64(0) + if entry.mode == object.FileModeRegular || entry.mode == object.FileModeExecutable || entry.mode == object.FileModeSymlink { + var err error + size, err = entry.blobSize(treeFS.resolver) + if err != nil { + return nil, err + } + } + + var sys any + if entry.treeEntry != nil { + sys = *entry.treeEntry + } + + return &treeFSInfo{ + name: entry.name, + mode: treeFSEntryMode(entry.mode), + size: size, + sys: sys, + isDir: entry.isDir(), + }, nil +} diff --git a/object/resolve/treefs_new.go b/object/resolve/treefs_new.go new file mode 100644 index 00000000..a51c06af --- /dev/null +++ b/object/resolve/treefs_new.go @@ -0,0 +1,17 @@ +package resolve + +import "codeberg.org/lindenii/furgit/objectid" + +// TreeFS returns one new filesystem view rooted at root, which may be any +// tree-ish object accepted by PeelToTreeID. +func (r *Resolver) TreeFS(root objectid.ObjectID) (*TreeFS, error) { + rootTree, err := r.PeelToTreeID(root) + if err != nil { + return nil, err + } + + return &TreeFS{ + resolver: r, + rootTree: rootTree, + }, nil +} diff --git a/object/resolve/treefs_op.go b/object/resolve/treefs_op.go new file mode 100644 index 00000000..ed93ec85 --- /dev/null +++ b/object/resolve/treefs_op.go @@ -0,0 +1,28 @@ +package resolve + +type treeFSOp uint8 + +const ( + treeFSOpOpen treeFSOp = iota + treeFSOpReadFile + treeFSOpReadDir + treeFSOpStat + treeFSOpSub +) + +func (op treeFSOp) pathErrorOp() string { + switch op { + case treeFSOpOpen: + return "open" + case treeFSOpReadFile: + return "readfile" + case treeFSOpReadDir: + return "readdir" + case treeFSOpStat: + return "stat" + case treeFSOpSub: + return "sub" + default: + return "treefs" + } +} diff --git a/object/resolve/treefs_open.go b/object/resolve/treefs_open.go new file mode 100644 index 00000000..76bd5e4f --- /dev/null +++ b/object/resolve/treefs_open.go @@ -0,0 +1,121 @@ +package resolve + +import ( + "fmt" + "io" + "io/fs" + + "codeberg.org/lindenii/furgit/object" +) + +func (treeFS *TreeFS) Open(name string) (fs.File, error) { + entry, err := treeFS.resolvePath(treeFSOpOpen, name) + if err != nil { + return nil, err + } + + info, err := treeFS.statEntry(entry) + if err != nil { + return nil, treeFSPathError(treeFSOpOpen, name, err) + } + + if entry.isDir() { + treeID, err := entry.subtreeID() + if err != nil { + return nil, treeFSPathError(treeFSOpOpen, name, err) + } + + tree, err := treeFS.resolver.ExactTree(treeID) + if err != nil { + return nil, treeFSPathError(treeFSOpOpen, name, err) + } + + entries := make([]fs.DirEntry, 0, len(tree.Object().Entries)) + for _, child := range tree.Object().Entries { + childEntry := treeEntryValue{ + name: string(child.Name), + mode: child.Mode, + objectID: child.ID, + treeEntry: &child, + } + + childInfo, err := treeFS.statEntry(childEntry) + if err != nil { + return nil, treeFSPathError(treeFSOpOpen, name, err) + } + + entries = append(entries, childInfo) + } + + return &treeFSDir{ + info: info, + entries: entries, + }, nil + } + + if entry.mode == object.FileModeGitlink { + return nil, treeFSPathError(treeFSOpOpen, name, fmt.Errorf("object/resolve: gitlink entries are not readable as files")) + } + + reader, _, err := treeFS.resolver.ExactBlobReader(entry.objectID) + if err != nil { + return nil, treeFSPathError(treeFSOpOpen, name, err) + } + + return &treeFSBlob{ + info: info, + reader: reader, + }, nil +} + +type treeFSBlob struct { + info *treeFSInfo + reader io.ReadCloser +} + +var _ fs.File = (*treeFSBlob)(nil) + +func (file *treeFSBlob) Stat() (fs.FileInfo, error) { return file.info, nil } +func (file *treeFSBlob) Read(p []byte) (int, error) { return file.reader.Read(p) } +func (file *treeFSBlob) Close() error { return file.reader.Close() } + +type treeFSDir struct { + info *treeFSInfo + entries []fs.DirEntry + offset int +} + +var ( + _ fs.File = (*treeFSDir)(nil) + _ fs.ReadDirFile = (*treeFSDir)(nil) +) + +func (dir *treeFSDir) Stat() (fs.FileInfo, error) { return dir.info, nil } +func (dir *treeFSDir) Close() error { return nil } + +func (dir *treeFSDir) Read(_ []byte) (int, error) { + return 0, fs.ErrInvalid +} + +func (dir *treeFSDir) ReadDir(n int) ([]fs.DirEntry, error) { + if dir.offset >= len(dir.entries) && n > 0 { + return nil, io.EOF + } + + if n <= 0 { + out := append([]fs.DirEntry(nil), dir.entries[dir.offset:]...) + dir.offset = len(dir.entries) + + return out, nil + } + + end := dir.offset + n + if end > len(dir.entries) { + end = len(dir.entries) + } + + out := append([]fs.DirEntry(nil), dir.entries[dir.offset:end]...) + dir.offset = end + + return out, nil +} diff --git a/object/resolve/treefs_path.go b/object/resolve/treefs_path.go new file mode 100644 index 00000000..cb5735d2 --- /dev/null +++ b/object/resolve/treefs_path.go @@ -0,0 +1,28 @@ +package resolve + +import ( + "io/fs" + "strings" +) + +func treeFSValidPath(name string) bool { + return name == "." || fs.ValidPath(name) +} + +func treeFSSplitPath(name string) [][]byte { + if name == "." { + return nil + } + + parts := strings.Split(name, "/") + out := make([][]byte, len(parts)) + for i, part := range parts { + out[i] = []byte(part) + } + + return out +} + +func treeFSPathError(op treeFSOp, path string, err error) error { + return &fs.PathError{Op: op.pathErrorOp(), Path: path, Err: err} +} diff --git a/object/resolve/treefs_readdir.go b/object/resolve/treefs_readdir.go new file mode 100644 index 00000000..293dcec4 --- /dev/null +++ b/object/resolve/treefs_readdir.go @@ -0,0 +1,19 @@ +package resolve + +import "io/fs" + +func (treeFS *TreeFS) ReadDir(name string) ([]fs.DirEntry, error) { + file, err := treeFS.Open(name) + if err != nil { + return nil, err + } + + defer func() { _ = file.Close() }() + + readDirFile, ok := file.(fs.ReadDirFile) + if !ok { + return nil, treeFSPathError(treeFSOpReadDir, name, fs.ErrInvalid) + } + + return readDirFile.ReadDir(-1) +} diff --git a/object/resolve/treefs_readfile.go b/object/resolve/treefs_readfile.go new file mode 100644 index 00000000..72af931c --- /dev/null +++ b/object/resolve/treefs_readfile.go @@ -0,0 +1,37 @@ +package resolve + +import ( + "fmt" + "io" + + "codeberg.org/lindenii/furgit/object" +) + +func (treeFS *TreeFS) ReadFile(name string) ([]byte, error) { + entry, err := treeFS.resolvePath(treeFSOpReadFile, name) + if err != nil { + return nil, err + } + + if entry.isDir() { + return nil, treeFSPathError(treeFSOpReadFile, name, fmt.Errorf("is a directory")) + } + + if entry.mode == object.FileModeGitlink { + return nil, treeFSPathError(treeFSOpReadFile, name, fmt.Errorf("object/resolve: gitlink entries are not readable as files")) + } + + reader, _, err := treeFS.resolver.ExactBlobReader(entry.objectID) + if err != nil { + return nil, treeFSPathError(treeFSOpReadFile, name, err) + } + + defer func() { _ = reader.Close() }() + + data, err := io.ReadAll(reader) + if err != nil { + return nil, treeFSPathError(treeFSOpReadFile, name, err) + } + + return data, nil +} diff --git a/object/resolve/treefs_stat.go b/object/resolve/treefs_stat.go new file mode 100644 index 00000000..5fbecac6 --- /dev/null +++ b/object/resolve/treefs_stat.go @@ -0,0 +1,17 @@ +package resolve + +import "io/fs" + +func (treeFS *TreeFS) Stat(name string) (fs.FileInfo, error) { + entry, err := treeFS.resolvePath(treeFSOpStat, name) + if err != nil { + return nil, err + } + + info, err := treeFS.statEntry(entry) + if err != nil { + return nil, treeFSPathError(treeFSOpStat, name, err) + } + + return info, nil +} diff --git a/object/resolve/treefs_sub.go b/object/resolve/treefs_sub.go new file mode 100644 index 00000000..d8832818 --- /dev/null +++ b/object/resolve/treefs_sub.go @@ -0,0 +1,21 @@ +package resolve + +import "io/fs" + +func (treeFS *TreeFS) Sub(dir string) (fs.FS, error) { + entry, err := treeFS.resolvePath(treeFSOpSub, dir) + if err != nil { + return nil, err + } + + treeID, err := entry.subtreeID() + if err != nil { + return nil, treeFSPathError(treeFSOpSub, dir, fs.ErrInvalid) + } + + return &TreeFS{ + resolver: treeFS.resolver, + rootTree: treeID, + rootEntry: entry.treeEntry, + }, nil +} diff --git a/object/resolve/treefs_test.go b/object/resolve/treefs_test.go new file mode 100644 index 00000000..44ae0544 --- /dev/null +++ b/object/resolve/treefs_test.go @@ -0,0 +1,109 @@ +package resolve_test + +import ( + "errors" + "io/fs" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/object/resolve" + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/repository" +) + +func TestTreeFS(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { + t.Parallel() + + repoData := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) + repoData.WriteFile(t, "plain.txt", []byte("plain\n"), 0o644) + repoData.WriteFileAll(t, "dir/exec.sh", []byte("#!/bin/sh\nexit 0\n"), 0o755, 0o755) + repoData.SymbolicRef(t, "HEAD", "refs/heads/main") + _ = repoData.Run(t, "add", ".") + treeHex := repoData.Run(t, "write-tree") + treeID, err := objectid.ParseHex(algo, treeHex) + if err != nil { + t.Fatalf("ParseHex(write-tree): %v", err) + } + + commitID := repoData.CommitTree(t, treeID, "treefs") + + root := repoData.OpenGitRoot(t) + + repo, err := repository.Open(root) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + defer func() { _ = repo.Close() }() + + resolver := resolve.New(repo.Objects()) + treeFS, err := resolver.TreeFS(commitID) + if err != nil { + t.Fatalf("resolver.TreeFS: %v", err) + } + + content, err := treeFS.ReadFile("plain.txt") + if err != nil { + t.Fatalf("ReadFile(plain.txt): %v", err) + } + + if string(content) != "plain\n" { + t.Fatalf("ReadFile(plain.txt) = %q, want %q", string(content), "plain\n") + } + + entries, err := treeFS.ReadDir(".") + if err != nil { + t.Fatalf("ReadDir(.): %v", err) + } + + if len(entries) != 2 { + t.Fatalf("len(ReadDir(.)) = %d, want 2", len(entries)) + } + + info, err := treeFS.Stat("plain.txt") + if err != nil { + t.Fatalf("Stat(plain.txt): %v", err) + } + + entry, ok := info.Sys().(object.TreeEntry) + if !ok { + t.Fatalf("Stat(plain.txt).Sys() type = %T, want object.TreeEntry", info.Sys()) + } + + if entry.Mode != object.FileModeRegular { + t.Fatalf("Stat(plain.txt).Sys().Mode = %o, want %o", entry.Mode, object.FileModeRegular) + } + + subFS, err := treeFS.Sub("dir") + if err != nil { + t.Fatalf("Sub(dir): %v", err) + } + + subReadFileFS, ok := subFS.(fs.ReadFileFS) + if !ok { + t.Fatalf("Sub(dir) type does not implement fs.ReadFileFS") + } + + subContent, err := subReadFileFS.ReadFile("exec.sh") + if err != nil { + t.Fatalf("Sub(dir).ReadFile(exec.sh): %v", err) + } + + if string(subContent) != "#!/bin/sh\nexit 0\n" { + t.Fatalf("Sub(dir).ReadFile(exec.sh) = %q", string(subContent)) + } + + _, err = treeFS.ReadFile("dir") + if err == nil { + t.Fatal("ReadFile(dir) unexpectedly succeeded") + } + + var pathErr *fs.PathError + if !errors.As(err, &pathErr) { + t.Fatalf("ReadFile(dir) err type = %T, want *fs.PathError", err) + } + }) +} |
