1// Package memfs is a RAM file server: Plan 9's ramfs, in a realm's heap.2//3// It is the reference implementation of gno.land/p/moul/x/plan9/ninep's File4// and Mutable, and the tree a namespace server hands out as a user's private5// root. Children live in an avl.Tree, so a directory listing is ordered by6// name and therefore identical on every validating node; a map would make7// Render a consensus bug.8//9// Mutable is IN-REALM ONLY. A non-crossing method runs in the caller's frame,10// so a foreign realm calling Create or Write here would be mutating objects it11// does not own. Reads (the ninep.File half) are safe from anywhere, which is12// what makes a memfs tree mountable into somebody else's namespace.13//14// The caller supplies the clock (a block height) on every mutation rather than15// the tree reading chain state itself, so the same tree is exercisable in a16// plain unit test.17//18// NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research19// Center at Bell Labs; the name and the marks are theirs, and the copyright is20// held by the Plan 9 Foundation (https://p9f.org). This package is not21// affiliated with, endorsed by, or sponsored by them, and contains no Plan 922// code: it borrows the vocabulary so that the design reads without a glossary,23// and it is an homage, asking what that ecosystem's spirit looks like on a24// chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.25package memfs2627import (28 "strings"2930 "gno.land/p/nt/avl/v0"3132 ninep "gno.land/p/moul/x/plan9/ninep/v0"33)3435// FS is a mutable in-memory file tree.36type FS struct {37 root *node38 next uint64 // qid path allocator, unique for this server's lifetime39 uid string40}4142type node struct {43 fs *FS44 name string45 perm ninep.Perm46 qid ninep.Qid47 data string48 children *avl.Tree // name -> *node; nil for a plain file49 mtime int6450 uid string51 gid string52 muid string53}5455// New returns an empty file server owned by uid, with its root created at56// block height now.57func New(uid string, now int64) *FS {58 fs := &FS{uid: uid}59 fs.root = fs.newNode("/", ninep.DirPerm, now)60 return fs61}6263// Root returns the server's root directory.64func (fs *FS) Root() ninep.File { return fs.root }6566// RootMutable returns the root as a Mutable. Only call it from the realm that67// owns this FS.68func (fs *FS) RootMutable() ninep.Mutable { return fs.root }6970// Uid returns the owner this server stamps on new files.71func (fs *FS) Uid() string { return fs.uid }7273func (fs *FS) newNode(name string, perm ninep.Perm, now int64) *node {74 fs.next++75 qt := ninep.QTFILE76 var kids *avl.Tree77 if perm.IsDir() {78 qt = ninep.QTDIR79 kids = avl.NewTree()80 }81 return &node{82 fs: fs,83 name: name,84 perm: perm,85 qid: ninep.Qid{Type: qt, Version: 0, Path: fs.next},86 children: kids,87 mtime: now,88 uid: fs.uid,89 gid: fs.uid,90 muid: fs.uid,91 }92}9394// Stat implements ninep.File.95func (n *node) Stat() ninep.Stat {96 length := int64(len(n.data))97 if n.perm.IsDir() {98 length = 0 // 9P reports zero for a directory99 }100 return ninep.Stat{101 Qid: n.qid,102 Mode: n.perm,103 Mtime: n.mtime,104 Length: length,105 Name: n.name,106 Uid: n.uid,107 Gid: n.gid,108 Muid: n.muid,109 }110}111112// Walk implements ninep.File.113func (n *node) Walk(name string) (ninep.File, error) {114 if !n.perm.IsDir() {115 return nil, ninep.ErrNotDir116 }117 if !ninep.ValidName(name) {118 return nil, ninep.ErrBadName119 }120 v := n.children.Get(name) // avl.Get returns ONE value in gno121 if v == nil {122 return nil, ninep.ErrNotExist123 }124 return v.(*node), nil125}126127// Read implements ninep.File.128func (n *node) Read(off, count int64) (string, error) {129 if n.perm.IsDir() {130 return "", ninep.ErrIsDir131 }132 return ninep.Slice(n.data, off, count), nil133}134135// ReadDir implements ninep.File.136func (n *node) ReadDir() ([]ninep.Stat, error) {137 if !n.perm.IsDir() {138 return nil, ninep.ErrNotDir139 }140 out := []ninep.Stat{}141 n.children.Iterate("", "", func(_ string, v any) bool {142 out = append(out, v.(*node).Stat())143 return false144 })145 return out, nil146}147148// Create implements ninep.Mutable.149func (n *node) Create(name string, perm ninep.Perm, now int64) (ninep.File, error) {150 if !n.perm.IsDir() {151 return nil, ninep.ErrNotDir152 }153 if !ninep.ValidName(name) {154 return nil, ninep.ErrBadName155 }156 if n.children.Has(name) {157 return nil, ninep.ErrExist158 }159 child := n.fs.newNode(name, perm, now)160 n.children.Set(name, child)161 n.touch(now)162 return child, nil163}164165// Remove implements ninep.Mutable. A non-empty directory is refused, as in166// Plan 9.167func (n *node) Remove(name string) error {168 if !n.perm.IsDir() {169 return ninep.ErrNotDir170 }171 v := n.children.Get(name)172 if v == nil {173 return ninep.ErrNotExist174 }175 child := v.(*node)176 if child.perm.IsDir() && child.children.Size() > 0 {177 return ninep.ErrNotEmpty178 }179 n.children.Remove(name)180 n.touch(n.mtime)181 return nil182}183184// Write implements ninep.Mutable. Writing past the end extends the file with185// NUL bytes, as a 9P server does.186func (n *node) Write(off int64, data string, now int64) (int64, error) {187 if n.perm.IsDir() {188 return 0, ninep.ErrIsDir189 }190 if off < 0 {191 off = 0192 }193 if n.perm&ninep.DMAPPEND != 0 {194 off = int64(len(n.data))195 }196 cur := n.data197 if off > int64(len(cur)) {198 cur += strings.Repeat("\x00", int(off)-len(cur))199 }200 end := off + int64(len(data))201 tail := ""202 if end < int64(len(cur)) {203 tail = cur[end:]204 }205 n.data = cur[:off] + data + tail206 n.touch(now)207 return int64(len(data)), nil208}209210// Truncate implements ninep.Mutable.211func (n *node) Truncate(size int64, now int64) error {212 if n.perm.IsDir() {213 return ninep.ErrIsDir214 }215 if size < 0 {216 size = 0217 }218 switch {219 case size < int64(len(n.data)):220 n.data = n.data[:size]221 case size > int64(len(n.data)):222 n.data += strings.Repeat("\x00", int(size)-len(n.data))223 }224 n.touch(now)225 return nil226}227228// touch bumps the qid version and the mtime. A client that kept a qid can tell229// the file changed without reading it, which is the whole point of Qid.Version.230func (n *node) touch(now int64) {231 n.qid.Version++232 n.mtime = now233 n.muid = n.fs.uid234}235236// MkdirAll creates p and every missing parent, and returns the leaf. An237// existing directory is not an error; an existing plain file on the path is.238func (fs *FS) MkdirAll(p string, now int64) (ninep.File, error) {239 cur := fs.root240 for _, e := range ninep.Elems(p) {241 v := cur.children.Get(e)242 if v == nil {243 f, err := cur.Create(e, ninep.DirPerm, now)244 if err != nil {245 return nil, err246 }247 cur = f.(*node)248 continue249 }250 cur = v.(*node)251 if !cur.perm.IsDir() {252 return nil, ninep.ErrNotDir253 }254 }255 return cur, nil256}257258// WriteFile creates or replaces the file at p, creating parent directories as259// needed. It is the seeding helper a realm wants at init.260func (fs *FS) WriteFile(p, data string, now int64) error {261 dir, err := fs.MkdirAll(ninep.Dir(p), now)262 if err != nil {263 return err264 }265 name := ninep.Base(p)266 d := dir.(*node)267 v := d.children.Get(name)268 if v == nil {269 f, err := d.Create(name, ninep.FilePerm, now)270 if err != nil {271 return err272 }273 _, err = f.(*node).Write(0, data, now)274 return err275 }276 target := v.(*node)277 if err := target.Truncate(0, now); err != nil {278 return err279 }280 _, err = target.Write(0, data, now)281 return err282}283Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.