blob: ab77e803170455b93004d2e5817d36d4d9169236 [file] [log] [blame]
Jan Schäre4c48542025-03-20 08:39:10 +00001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4// Package structfs defines a data structure for a file system, similar to the
5// [fs] package but based on structs instead of interfaces.
6//
7// The entire tree structure and directory entry metadata is stored in memory.
8// File content is represented with [Blob], and may come from various sources.
9package structfs
10
11import (
12 "io/fs"
13 "iter"
14 pathlib "path"
15 "strings"
16 "syscall"
17 "time"
18 "unicode/utf8"
19)
20
21// Tree represents a file system tree.
22type Tree []*Node
23
24// Node is a node in a file system tree, which is either a file or directory.
25type Node struct {
26 // Name of this node, which must be valid according to [ValidName].
27 Name string
28 // Mode contains the file type and permissions.
29 Mode fs.FileMode
30 // ModTime is the modification time.
31 ModTime time.Time
32 // Content is the file content, must be set for regular files.
33 Content Blob
34 // Children of a directory, must be empty if this is not a directory.
35 Children Tree
36 // Sys contains any system-specific directory entry fields.
37 //
38 // It should be accessed using interface type assertions, to allow combining
39 // information for multiple target systems with struct embedding.
40 Sys any
41}
42
43type Option func(*Node)
44
45// WithModTime sets the ModTime of the Node.
46func WithModTime(t time.Time) Option {
47 return func(n *Node) {
48 n.ModTime = t
49 }
50}
51
52const permMask = fs.ModePerm | fs.ModeSetuid | fs.ModeSetgid | fs.ModeSticky
53
54// WithPerm sets the permission bits of the Node.
55func WithPerm(perm fs.FileMode) Option {
56 return func(n *Node) {
57 n.Mode = (n.Mode & ^permMask) | (perm & permMask)
58 }
59}
60
61// WithSys sets the Sys field of the Node.
62func WithSys(sys any) Option {
63 return func(n *Node) {
64 n.Sys = sys
65 }
66}
67
68// File creates a regular file node with the given name and content.
69//
70// Permission defaults to 644.
71func File(name string, content Blob, opts ...Option) *Node {
72 n := &Node{
73 Name: name,
74 Mode: 0o644,
75 Content: content,
76 }
77 for _, f := range opts {
78 f(n)
79 }
80 return n
81}
82
83// Dir creates a directory node with the given name and children.
84//
85// Permission defaults to 755.
86func Dir(name string, children Tree, opts ...Option) *Node {
87 n := &Node{
88 Name: name,
89 Mode: fs.ModeDir | 0o755,
90 Children: children,
91 }
92 for _, f := range opts {
93 f(n)
94 }
95 return n
96}
97
98// PlaceFile creates parent directories if necessary and places a file with the
99// given content at the path. It fails if path already exists.
100func (t *Tree) PlaceFile(path string, content Blob, opts ...Option) error {
101 path, name, err := splitPlacePath(path)
102 if err != nil {
103 return err
104 }
105 return t.Place(path, File(name, content, opts...))
106}
107
108// PlaceDir creates parent directories if necessary and places a directory with
109// the given children at the path. It fails if path already exists.
110func (t *Tree) PlaceDir(path string, children Tree, opts ...Option) error {
111 path, name, err := splitPlacePath(path)
112 if err != nil {
113 return err
114 }
115 return t.Place(path, Dir(name, children, opts...))
116}
117
118func splitPlacePath(path string) (dir string, name string, err error) {
119 if !fs.ValidPath(path) || path == "." {
120 return "", "", &fs.PathError{Op: "place", Path: path, Err: fs.ErrInvalid}
121 }
122 dir, name = pathlib.Split(path)
123 if dir == "" {
124 dir = "."
125 } else {
126 dir = dir[:len(dir)-1]
127 }
128 return
129}
130
131// Place creates directories if necessary and places the node in the directory
132// at the path.
133//
134// The special path "." indicates the root.
135func (t *Tree) Place(path string, node *Node) error {
136 if !fs.ValidPath(path) {
137 return &fs.PathError{Op: "place", Path: path, Err: fs.ErrInvalid}
138 }
139 treeRef := t
140 if path != "." {
141 pathlen := 0
142 outer:
143 for name := range strings.SplitSeq(path, "/") {
144 pathlen += len(name) + 1
145 for _, nodeRef := range *treeRef {
146 if nodeRef.Name == name {
147 if !nodeRef.Mode.IsDir() {
148 return &fs.PathError{Op: "mkdir", Path: path[:pathlen-1], Err: syscall.ENOTDIR}
149 }
150 treeRef = &nodeRef.Children
151 continue outer
152 }
153 }
154 dir := Dir(name, nil)
155 *treeRef = append(*treeRef, dir)
156 treeRef = &dir.Children
157 }
158 }
159 for _, nodeRef := range *treeRef {
160 if nodeRef.Name == node.Name {
161 return &fs.PathError{Op: "place", Path: path + "/" + nodeRef.Name, Err: fs.ErrExist}
162 }
163 }
164 *treeRef = append(*treeRef, node)
165 return nil
166}
167
168// Walk returns an iterator over all nodes in the tree in DFS pre-order.
169// The key is the path of the node.
170//
171// Entries with invalid name are skipped.
172func (t Tree) Walk() iter.Seq2[string, *Node] {
173 return func(yield func(string, *Node) bool) {
174 walk(t, ".", yield)
175 }
176}
177
178func walk(t Tree, path string, yield func(string, *Node) bool) bool {
179 for _, node := range t {
180 if !ValidName(node.Name) {
181 // Skip entries with invalid name.
182 continue
183 }
184 nodePath := node.Name
185 if path != "." {
186 nodePath = path + "/" + nodePath
187 }
188 if !yield(nodePath, node) {
189 return false
190 }
191 if node.Mode.IsDir() {
192 if !walk(node.Children, nodePath, yield) {
193 return false
194 }
195 }
196 }
197 return true
198}
199
200// ValidName reports whether the given name is a valid node name.
201//
202// The name must be UTF-8-encoded, must not be empty, "." or "..", and must not
203// contain "/". These are the same rules as for a path element in [fs.ValidPath].
204func ValidName(name string) bool {
205 if !utf8.ValidString(name) {
206 return false
207 }
208 if name == "" || name == "." || name == ".." {
209 return false
210 }
211 if strings.ContainsRune(name, '/') {
212 return false
213 }
214 return true
215}