diff --git a/examples/gno.land/r/demo/boards/z_4_filetest.gno b/examples/gno.land/r/demo/boards/z_4_filetest.gno index 749566ea5bc..3fb3d2653b4 100644 --- a/examples/gno.land/r/demo/boards/z_4_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_4_filetest.gno @@ -374,7 +374,7 @@ func main() { // "Escaped": true, // "ObjectID": "336074805fc853987abe6f7fe3ad97a6a6f3077a:2" // }, -// "Index": "188", +// "Index": "191", // "TV": null // } // } @@ -541,7 +541,7 @@ func main() { // }, // "V": { // "@type": "/gno.RefValue", -// "Hash": "8164abed5231309c88497013f7da72a1b5d427b0", +// "Hash": "40c644a23c5104a1d6695e17aeac3939dcfecf70", // "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:115" // } // }, @@ -847,7 +847,7 @@ func main() { // }, // "V": { // "@type": "/gno.RefValue", -// "Hash": "5b4b593f1d4b37cb99166247ea28174f91087fdd", +// "Hash": "8dbf25500e2f04e50584894e606ad0788fe6920d", // "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:82" // } // }, @@ -865,7 +865,7 @@ func main() { // }, // "V": { // "@type": "/gno.RefValue", -// "Hash": "7e9fd9bb5e90a06c7751585cd80f23aedddde25b", +// "Hash": "64d61e4f065f6d0760e37b76fbd832db903b68ec", // "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:83" // } // }, diff --git a/gnovm/stdlibs/internal/oserror/errors.gno b/gnovm/stdlibs/internal/oserror/errors.gno new file mode 100644 index 00000000000..f7ff346c132 --- /dev/null +++ b/gnovm/stdlibs/internal/oserror/errors.gno @@ -0,0 +1,11 @@ +package oserror + +import "errors" + +var ( + ErrInvalid = errors.New("invalid argument") + ErrPermission = errors.New("permission denied") + ErrExist = errors.New("file already exists") + ErrNotExist = errors.New("file does not exist") + ErrClosed = errors.New("file closed") +) diff --git a/gnovm/stdlibs/io/fs/format.gno b/gnovm/stdlibs/io/fs/format.gno new file mode 100644 index 00000000000..60b40df1e83 --- /dev/null +++ b/gnovm/stdlibs/io/fs/format.gno @@ -0,0 +1,76 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fs + +import ( + "time" +) + +// FormatFileInfo returns a formatted version of info for human readability. +// Implementations of [FileInfo] can call this from a String method. +// The output for a file named "hello.go", 100 bytes, mode 0o644, created +// January 1, 1970 at noon is +// +// -rw-r--r-- 100 1970-01-01 12:00:00 hello.go +func FormatFileInfo(info FileInfo) string { + name := info.Name() + b := make([]byte, 0, 40+len(name)) + b = append(b, info.Mode().String()...) + b = append(b, ' ') + + size := info.Size() + var usize uint64 + if size >= 0 { + usize = uint64(size) + } else { + b = append(b, '-') + usize = uint64(-size) + } + var buf [20]byte + i := len(buf) - 1 + for usize >= 10 { + q := usize / 10 + buf[i] = byte('0' + usize - q*10) + i-- + usize = q + } + buf[i] = byte('0' + usize) + b = append(b, buf[i:]...) + b = append(b, ' ') + + b = append(b, info.ModTime().Format(time.DateTime)...) + b = append(b, ' ') + + b = append(b, name...) + if info.IsDir() { + b = append(b, '/') + } + + return string(b) +} + +// FormatDirEntry returns a formatted version of dir for human readability. +// Implementations of [DirEntry] can call this from a String method. +// The outputs for a directory named subdir and a file named hello.go are: +// +// d subdir/ +// - hello.go +func FormatDirEntry(dir DirEntry) string { + name := dir.Name() + b := make([]byte, 0, 5+len(name)) + + // The Type method does not return any permission bits, + // so strip them from the string. + mode := dir.Type().String() + mode = mode[:len(mode)-9] + + b = append(b, mode...) + b = append(b, ' ') + b = append(b, name...) + if dir.IsDir() { + b = append(b, '/') + } + return string(b) +} diff --git a/gnovm/stdlibs/io/fs/format_test.gno b/gnovm/stdlibs/io/fs/format_test.gno new file mode 100644 index 00000000000..1448dcc6816 --- /dev/null +++ b/gnovm/stdlibs/io/fs/format_test.gno @@ -0,0 +1,122 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fs_test + +import ( + "io/fs" + "testing" + "time" +) + +// formatTest implements FileInfo to test FormatFileInfo, +// and implements DirEntry to test FormatDirEntry. +type formatTest struct { + name string + size int64 + mode fs.FileMode + modTime time.Time + isDir bool +} + +func (fs *formatTest) Name() string { + return fs.name +} + +func (fs *formatTest) Size() int64 { + return fs.size +} + +func (fs *formatTest) Mode() fs.FileMode { + return fs.mode +} + +func (fs *formatTest) ModTime() time.Time { + return fs.modTime +} + +func (fs *formatTest) IsDir() bool { + return fs.isDir +} + +func (fs *formatTest) Sys() interface{} { + return nil +} + +func (fs *formatTest) Type() fs.FileMode { + return fs.mode.Type() +} + +func (fs *formatTest) Info() (fs.FileInfo, error) { + return fs, nil +} + +var formatTests = []struct { + input formatTest + wantFileInfo string + wantDirEntry string +}{ + { + formatTest{ + name: "hello.go", + size: 100, + mode: 0o644, + modTime: time.Date(1970, time.January, 1, 12, 0, 0, 0, time.UTC), + isDir: false, + }, + "-rw-r--r-- 100 1970-01-01 12:00:00 hello.go", + "- hello.go", + }, + { + formatTest{ + name: "home/gopher", + size: 0, + mode: fs.ModeDir | 0o755, + modTime: time.Date(1970, time.January, 1, 12, 0, 0, 0, time.UTC), + isDir: true, + }, + "drwxr-xr-x 0 1970-01-01 12:00:00 home/gopher/", + "d home/gopher/", + }, + { + formatTest{ + name: "big", + size: 0x7fffffffffffffff, + mode: fs.ModeIrregular | 0o644, + modTime: time.Date(1970, time.January, 1, 12, 0, 0, 0, time.UTC), + isDir: false, + }, + "?rw-r--r-- 9223372036854775807 1970-01-01 12:00:00 big", + "? big", + }, + { + formatTest{ + name: "small", + size: -0x8000000000000000, + mode: fs.ModeSocket | fs.ModeSetuid | 0o644, + modTime: time.Date(1970, time.January, 1, 12, 0, 0, 0, time.UTC), + isDir: false, + }, + "Surw-r--r-- -9223372036854775808 1970-01-01 12:00:00 small", + "S small", + }, +} + +func TestFormatFileInfo(t *testing.T) { + for i, test := range formatTests { + got := fs.FormatFileInfo(&test.input) + if got != test.wantFileInfo { + t.Errorf("%d: FormatFileInfo(%#v) = %q, want %q", i, test.input, got, test.wantFileInfo) + } + } +} + +func TestFormatDirEntry(t *testing.T) { + for i, test := range formatTests { + got := fs.FormatDirEntry(&test.input) + if got != test.wantDirEntry { + t.Errorf("%d: FormatDirEntry(%#v) = %q, want %q", i, test.input, got, test.wantDirEntry) + } + } +} diff --git a/gnovm/stdlibs/io/fs/fs.gno b/gnovm/stdlibs/io/fs/fs.gno new file mode 100644 index 00000000000..142e40e3df7 --- /dev/null +++ b/gnovm/stdlibs/io/fs/fs.gno @@ -0,0 +1,259 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package fs defines basic interfaces to a file system. +// A file system can be provided by the host operating system +// but also by other packages. +package fs + +import ( + "time" + "unicode/utf8" + + "internal/oserror" +) + +// An FS provides access to a hierarchical file system. +// +// The FS interface is the minimum implementation required of the file system. +// A file system may implement additional interfaces, +// such as ReadFileFS, to provide additional or optimized functionality. +type FS interface { + // Open opens the named file. + // + // When Open returns an error, it should be of type *PathError + // with the Op field set to "open", the Path field set to name, + // and the Err field describing the problem. + // + // Open should reject attempts to open names that do not satisfy + // ValidPath(name), returning a *PathError with Err set to + // ErrInvalid or ErrNotExist. + Open(name string) (File, error) +} + +// ValidPath reports whether the given path name +// is valid for use in a call to Open. +// +// Path names passed to open are UTF-8-encoded, +// unrooted, slash-separated sequences of path elements, like “x/y/z”. +// Path names must not contain an element that is “.” or “..” or the empty string, +// except for the special case that the root directory is named “.”. +// Paths must not start or end with a slash: “/x” and “x/” are invalid. +// +// Note that paths are slash-separated on all systems, even Windows. +// Paths containing other characters such as backslash and colon +// are accepted as valid, but those characters must never be +// interpreted by an FS implementation as path element separators. +func ValidPath(name string) bool { + if !utf8.ValidString(name) { + return false + } + + if name == "." { + // special case + return true + } + + // Iterate over elements in name, checking each. + for { + i := 0 + for i < len(name) && name[i] != '/' { + i++ + } + elem := name[:i] + if elem == "" || elem == "." || elem == ".." { + return false + } + if i == len(name) { + return true // reached clean ending + } + name = name[i+1:] + } +} + +// A File provides access to a single file. +// The File interface is the minimum implementation required of the file. +// Directory files should also implement ReadDirFile. +// A file may implement io.ReaderAt or io.Seeker as optimizations. +type File interface { + Stat() (FileInfo, error) + Read([]byte) (int, error) + Close() error +} + +// A DirEntry is an entry read from a directory +// (using the ReadDir function or a ReadDirFile's ReadDir method). +type DirEntry interface { + // Name returns the name of the file (or subdirectory) described by the entry. + // This name is only the final element of the path (the base name), not the entire path. + // For example, Name would return "hello.go" not "home/gopher/hello.go". + Name() string + + // IsDir reports whether the entry describes a directory. + IsDir() bool + + // Type returns the type bits for the entry. + // The type bits are a subset of the usual FileMode bits, those returned by the FileMode.Type method. + Type() FileMode + + // Info returns the FileInfo for the file or subdirectory described by the entry. + // The returned FileInfo may be from the time of the original directory read + // or from the time of the call to Info. If the file has been removed or renamed + // since the directory read, Info may return an error satisfying errors.Is(err, ErrNotExist). + // If the entry denotes a symbolic link, Info reports the information about the link itself, + // not the link's target. + Info() (FileInfo, error) +} + +// A ReadDirFile is a directory file whose entries can be read with the ReadDir method. +// Every directory file should implement this interface. +// (It is permissible for any file to implement this interface, +// but if so ReadDir should return an error for non-directories.) +type ReadDirFile interface { + File + + // ReadDir reads the contents of the directory and returns + // a slice of up to n DirEntry values in directory order. + // Subsequent calls on the same file will yield further DirEntry values. + // + // If n > 0, ReadDir returns at most n DirEntry structures. + // In this case, if ReadDir returns an empty slice, it will return + // a non-nil error explaining why. + // At the end of a directory, the error is io.EOF. + // (ReadDir must return io.EOF itself, not an error wrapping io.EOF.) + // + // If n <= 0, ReadDir returns all the DirEntry values from the directory + // in a single slice. In this case, if ReadDir succeeds (reads all the way + // to the end of the directory), it returns the slice and a nil error. + // If it encounters an error before the end of the directory, + // ReadDir returns the DirEntry list read until that point and a non-nil error. + ReadDir(n int) ([]DirEntry, error) +} + +// Generic file system errors. +// Errors returned by file systems can be tested against these errors +// using errors.Is. +var ( + ErrInvalid = errInvalid() // "invalid argument" + ErrPermission = errPermission() // "permission denied" + ErrExist = errExist() // "file already exists" + ErrNotExist = errNotExist() // "file does not exist" + ErrClosed = errClosed() // "file already closed" +) + +func errInvalid() error { return oserror.ErrInvalid } +func errPermission() error { return oserror.ErrPermission } +func errExist() error { return oserror.ErrExist } +func errNotExist() error { return oserror.ErrNotExist } +func errClosed() error { return oserror.ErrClosed } + +// A FileInfo describes a file and is returned by Stat. +type FileInfo interface { + Name() string // base name of the file + Size() int64 // length in bytes for regular files; system-dependent for others + Mode() FileMode // file mode bits + ModTime() time.Time // modification time + IsDir() bool // abbreviation for Mode().IsDir() + Sys() interface{} // underlying data source (can return nil) +} + +// A FileMode represents a file's mode and permission bits. +// The bits have the same definition on all systems, so that +// information about files can be moved from one system +// to another portably. Not all bits apply to all systems. +// The only required bit is ModeDir for directories. +type FileMode uint32 + +// The defined file mode bits are the most significant bits of the FileMode. +// The nine least-significant bits are the standard Unix rwxrwxrwx permissions. +// The values of these bits should be considered part of the public API and +// may be used in wire protocols or disk representations: they must not be +// changed, although new bits might be added. +const ( + // The single letters are the abbreviations + // used by the String method's formatting. + ModeDir FileMode = 1 << (32 - 1 - iota) // d: is a directory + ModeAppend // a: append-only + ModeExclusive // l: exclusive use + ModeTemporary // T: temporary file; Plan 9 only + ModeSymlink // L: symbolic link + ModeDevice // D: device file + ModeNamedPipe // p: named pipe (FIFO) + ModeSocket // S: Unix domain socket + ModeSetuid // u: setuid + ModeSetgid // g: setgid + ModeCharDevice // c: Unix character device, when ModeDevice is set + ModeSticky // t: sticky + ModeIrregular // ?: non-regular file; nothing else is known about this file + + // Mask for the type bits. For regular files, none will be set. + ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular + + ModePerm FileMode = 0o777 // Unix permission bits +) + +func (m FileMode) String() string { + const str = "dalTLDpSugct?" + var buf [32]byte // Mode is uint32. + w := 0 + for i, c := range str { + if m&(1< pathSeparatorsLimit { + return nil, path.ErrBadPattern + } + if fsys, ok := fsys.(GlobFS); ok { + return fsys.Glob(pattern) + } + + // Check pattern is well-formed. + if _, err := path.Match(pattern, ""); err != nil { + return nil, err + } + if !hasMeta(pattern) { + if _, err = Stat(fsys, pattern); err != nil { + return nil, nil + } + return []string{pattern}, nil + } + + dir, file := path.Split(pattern) + dir = cleanGlobPath(dir) + + if !hasMeta(dir) { + return glob(fsys, dir, file, nil) + } + + // Prevent infinite recursion. See issue 15879. + if dir == pattern { + return nil, path.ErrBadPattern + } + + var m []string + m, err = globWithLimit(fsys, dir, depth+1) + if err != nil { + return nil, err + } + for _, d := range m { + matches, err = glob(fsys, d, file, matches) + if err != nil { + return + } + } + return +} + +// cleanGlobPath prepares path for glob matching. +func cleanGlobPath(path string) string { + switch path { + case "": + return "." + default: + return path[0 : len(path)-1] // chop off trailing separator + } +} + +// glob searches for files matching pattern in the directory dir +// and appends them to matches, returning the updated slice. +// If the directory cannot be opened, glob returns the existing matches. +// New matches are added in lexicographical order. +func glob(fs FS, dir, pattern string, matches []string) (m []string, e error) { + m = matches + infos, err := ReadDir(fs, dir) + if err != nil { + return // ignore I/O error + } + + for _, info := range infos { + n := info.Name() + matched, err := path.Match(pattern, n) + if err != nil { + return m, err + } + if matched { + m = append(m, path.Join(dir, n)) + } + } + return +} + +// hasMeta reports whether path contains any of the magic characters +// recognized by path.Match. +func hasMeta(path string) bool { + for i := 0; i < len(path); i++ { + switch path[i] { + case '*', '?', '[', '\\': + return true + } + } + return false +} diff --git a/gnovm/stdlibs/io/fs/readdir.gno b/gnovm/stdlibs/io/fs/readdir.gno new file mode 100644 index 00000000000..4ee5b3e21d8 --- /dev/null +++ b/gnovm/stdlibs/io/fs/readdir.gno @@ -0,0 +1,121 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package fs + +import ( + "errors" +) + +// XXX: To use `Sort` package, we need to implement `internal/reflectile`, which I think we should avoid for now. +// So, I've implemented basic sorting, which we'll need to fix later. +type ByName []DirEntry + +func (s ByName) Len() int { return len(s) } +func (s ByName) Less(i, j int) bool { return s[i].Name() < s[j].Name() } +func (s ByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func quickSort(entries []DirEntry, low, high int) { + if low >= high { + return + } + + pivot := entries[low] + i, j := low, high + for i < j { + for i < j && entries[j].Name() >= pivot.Name() { + j-- + } + + entries[i] = entries[j] + for i < j && entries[i].Name() <= pivot.Name() { + i++ + } + + entries[j] = entries[i] + } + + entries[i] = pivot + quickSort(entries, low, i-1) + quickSort(entries, i+1, high) +} + +func sortDirEntries(entries []DirEntry) { + quickSort(entries, 0, len(entries)-1) +} + +// ReadDirFS is the interface implemented by a file system +// that provides an optimized implementation of ReadDir. +type ReadDirFS interface { + FS + + // ReadDir reads the named directory + // and returns a list of directory entries sorted by filename. + ReadDir(name string) ([]DirEntry, error) +} + +// ReadDir reads the named directory +// and returns a list of directory entries sorted by filename. +// +// If fs implements ReadDirFS, ReadDir calls fs.ReadDir. +// Otherwise ReadDir calls fs.Open and uses ReadDir and Close +// on the returned file. +func ReadDir(fsys FS, name string) ([]DirEntry, error) { + if fsys, ok := fsys.(ReadDirFS); ok { + return fsys.ReadDir(name) + } + + file, err := fsys.Open(name) + if err != nil { + return nil, err + } + + defer file.Close() + + dir, ok := file.(ReadDirFile) + if !ok { + return nil, &PathError{Op: "readdir", Path: name, Err: errors.New("not implemented")} + } + + ls, err := dir.ReadDir(-1) + if err != nil { + return nil, err + } + + sortDirEntries(ls) + + return ls, err +} + +// dirInfo is a DirEntry based on a FileInfo. +type dirInfo struct { + fileInfo FileInfo +} + +func (d dirInfo) IsDir() bool { + return d.fileInfo.IsDir() +} + +func (d dirInfo) Type() FileMode { + return d.fileInfo.Mode().Type() +} + +func (d dirInfo) Info() (FileInfo, error) { + return d.fileInfo, nil +} + +func (d dirInfo) Name() string { + return d.fileInfo.Name() +} + +// func (d dirInfo) String() string { +// +// } + +func FileInfoToDirEntry(info FileInfo) DirEntry { + if info == nil { + return nil + } + + return dirInfo{fileInfo: info} +} diff --git a/gnovm/stdlibs/io/fs/readfile.gno b/gnovm/stdlibs/io/fs/readfile.gno new file mode 100644 index 00000000000..41ca5bfcf6e --- /dev/null +++ b/gnovm/stdlibs/io/fs/readfile.gno @@ -0,0 +1,66 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fs + +import "io" + +// ReadFileFS is the interface implemented by a file system +// that provides an optimized implementation of [ReadFile]. +type ReadFileFS interface { + FS + + // ReadFile reads the named file and returns its contents. + // A successful call returns a nil error, not io.EOF. + // (Because ReadFile reads the whole file, the expected EOF + // from the final Read is not treated as an error to be reported.) + // + // The caller is permitted to modify the returned byte slice. + // This method should return a copy of the underlying data. + ReadFile(name string) ([]byte, error) +} + +// ReadFile reads the named file from the file system fs and returns its contents. +// A successful call returns a nil error, not [io.EOF]. +// (Because ReadFile reads the whole file, the expected EOF +// from the final Read is not treated as an error to be reported.) +// +// If fs implements [ReadFileFS], ReadFile calls fs.ReadFile. +// Otherwise ReadFile calls fs.Open and uses Read and Close +// on the returned [File]. +func ReadFile(fsys FS, name string) ([]byte, error) { + if fsys, ok := fsys.(ReadFileFS); ok { + return fsys.ReadFile(name) + } + + file, err := fsys.Open(name) + if err != nil { + return nil, err + } + defer file.Close() + + var size int + if info, err := file.Stat(); err == nil { + size64 := info.Size() + if int64(int(size64)) == size64 { + size = int(size64) + } + } + + data := make([]byte, 0, size+1) + for { + if len(data) >= cap(data) { + d := append(data[:cap(data)], 0) + data = d[:len(data)] + } + n, err := file.Read(data[len(data):cap(data)]) + data = data[:len(data)+n] + if err != nil { + if err == io.EOF { + err = nil + } + return data, err + } + } +} diff --git a/gnovm/stdlibs/io/fs/stat.gno b/gnovm/stdlibs/io/fs/stat.gno new file mode 100644 index 00000000000..735a6e3281c --- /dev/null +++ b/gnovm/stdlibs/io/fs/stat.gno @@ -0,0 +1,31 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fs + +// A StatFS is a file system with a Stat method. +type StatFS interface { + FS + + // Stat returns a FileInfo describing the file. + // If there is an error, it should be of type *PathError. + Stat(name string) (FileInfo, error) +} + +// Stat returns a FileInfo describing the named file from the file system. +// +// If fs implements StatFS, Stat calls fs.Stat. +// Otherwise, Stat opens the file to stat it. +func Stat(fsys FS, name string) (FileInfo, error) { + if fsys, ok := fsys.(StatFS); ok { + return fsys.Stat(name) + } + + file, err := fsys.Open(name) + if err != nil { + return nil, err + } + defer file.Close() + return file.Stat() +} diff --git a/gnovm/stdlibs/io/fs/sub.gno b/gnovm/stdlibs/io/fs/sub.gno new file mode 100644 index 00000000000..4e1814a4686 --- /dev/null +++ b/gnovm/stdlibs/io/fs/sub.gno @@ -0,0 +1,134 @@ +package fs + +import ( + "errors" + "path" +) + +type SubFS interface { + FS + Sub(dir string) (FS, error) +} + +func Sub(fsys FS, dir string) (FS, error) { + if !ValidPath(dir) { + return nil, &PathError{Op: "sub", Path: dir, Err: errors.New("invalid name")} + } + + if dir == "." { + return fsys, nil + } + + if fsys, ok := fsys.(SubFS); ok { + return fsys.Sub(dir) + } + + return &subFS{fsys, dir}, nil +} + +type subFS struct { + fsys FS + dir string +} + +// fullName maps name to the fully-qualified name dir/name. +func (f *subFS) fullName(op string, name string) (string, error) { + if !ValidPath(name) { + return "", &PathError{Op: op, Path: name, Err: errors.New("invalid name")} + } + + return path.Join(f.dir, name), nil +} + +// shorten maps name, which should start with f.dir, back to the suffix after f.dir. +func (f *subFS) shorten(name string) (rel string, ok bool) { + if name == f.dir { + return ".", true + } + if len(name) >= len(f.dir)+2 && name[len(f.dir)] == '/' && name[:len(f.dir)] == f.dir { + return name[len(f.dir)+1:], true + } + return "", false +} + +// fixErr shortens any reported names in PathErrors by tripping f.dir. +func (f *subFS) fixErr(err error) error { + if e, ok := err.(*PathError); ok { + if short, ok := f.shorten(e.Path); ok { + e.Path = short + } + } + + return err +} + +func (f *subFS) Open(name string) (File, error) { + full, err := f.fullName("open", name) + if err != nil { + return nil, err + } + + file, err := f.fsys.Open(full) + return file, f.fixErr(err) +} + +func (f *subFS) ReadDir(name string) ([]DirEntry, error) { + full, err := f.fullName("read", name) + if err != nil { + return nil, err + } + + dir, err := ReadDir(f.fsys, full) + + return dir, f.fixErr(err) +} + +func (f *subFS) ReadFile(name string) ([]byte, error) { + full, err := f.fullName("read", name) + if err != nil { + return nil, err + } + + data, err := ReadFile(f.fsys, full) + return data, f.fixErr(err) +} + +func (f *subFS) Glob(pattern string) ([]string, error) { + if _, err := path.Match(pattern, ""); err != nil { + return nil, err + } + + if pattern == "." { + return []string{"."}, nil + } + + full := f.dir + "/" + pattern + list, err := Glob(f.fsys, full) + + for i, name := range list { + name, ok := f.shorten(name) + if !ok { + return nil, errors.New("invalid result from inner fsys Glob: " + name + " not in " + f.dir) // can't use fmt in this package + } + + list[i] = name + } + + return list, f.fixErr(err) +} + +func (f *subFS) Sub(dir string) (FS, error) { + if dir == "." { + return f, nil + } + + full, err := f.fullName("sub", dir) + if err != nil { + return nil, err + } + + return &subFS{ + fsys: f.fsys, + dir: full, + }, nil +} diff --git a/gnovm/stdlibs/io/fs/walk.gno b/gnovm/stdlibs/io/fs/walk.gno new file mode 100644 index 00000000000..48145d4cfce --- /dev/null +++ b/gnovm/stdlibs/io/fs/walk.gno @@ -0,0 +1,128 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fs + +import ( + "errors" + "path" +) + +// SkipDir is used as a return value from WalkDirFuncs to indicate that +// the directory named in the call is to be skipped. It is not returned +// as an error by any function. +var SkipDir = errors.New("skip this directory") + +// SkipAll is used as a return value from WalkDirFuncs to indicate that +// all remaining files and directories are to be skipped. It is not returned +// as an error by any function. +var SkipAll = errors.New("skip everything and stop the walk") + +// WalkDirFunc is the type of the function called by [WalkDir] to visit +// each file or directory. +// +// The path argument contains the argument to [WalkDir] as a prefix. +// That is, if WalkDir is called with root argument "dir" and finds a file +// named "a" in that directory, the walk function will be called with +// argument "dir/a". +// +// The d argument is the [DirEntry] for the named path. +// +// The error result returned by the function controls how [WalkDir] +// continues. If the function returns the special value [SkipDir], WalkDir +// skips the current directory (path if d.IsDir() is true, otherwise +// path's parent directory). If the function returns the special value +// [SkipAll], WalkDir skips all remaining files and directories. Otherwise, +// if the function returns a non-nil error, WalkDir stops entirely and +// returns that error. +// +// The err argument reports an error related to path, signaling that +// [WalkDir] will not walk into that directory. The function can decide how +// to handle that error; as described earlier, returning the error will +// cause WalkDir to stop walking the entire tree. +// +// [WalkDir] calls the function with a non-nil err argument in two cases. +// +// First, if the initial [Stat] on the root directory fails, WalkDir +// calls the function with path set to root, d set to nil, and err set to +// the error from [fs.Stat]. +// +// Second, if a directory's ReadDir method (see [ReadDirFile]) fails, WalkDir calls the +// function with path set to the directory's path, d set to an +// [DirEntry] describing the directory, and err set to the error from +// ReadDir. In this second case, the function is called twice with the +// path of the directory: the first call is before the directory read is +// attempted and has err set to nil, giving the function a chance to +// return [SkipDir] or [SkipAll] and avoid the ReadDir entirely. The second call +// is after a failed ReadDir and reports the error from ReadDir. +// (If ReadDir succeeds, there is no second call.) +// +// The differences between WalkDirFunc compared to [path/filepath.WalkFunc] are: +// +// - The second argument has type [DirEntry] instead of [FileInfo]. +// - The function is called before reading a directory, to allow [SkipDir] +// or [SkipAll] to bypass the directory read entirely or skip all remaining +// files and directories respectively. +// - If a directory read fails, the function is called a second time +// for that directory to report the error. +type WalkDirFunc func(path string, d DirEntry, err error) error + +// walkDir recursively descends path, calling walkDirFn. +func walkDir(fsys FS, name string, d DirEntry, walkDirFn WalkDirFunc) error { + if err := walkDirFn(name, d, nil); err != nil || !d.IsDir() { + if err == SkipDir && d.IsDir() { + // Successfully skipped directory. + err = nil + } + return err + } + + dirs, err := ReadDir(fsys, name) + if err != nil { + // Second call, to report ReadDir error. + err = walkDirFn(name, d, err) + if err != nil { + if err == SkipDir && d.IsDir() { + err = nil + } + return err + } + } + + for _, d1 := range dirs { + name1 := path.Join(name, d1.Name()) + if err := walkDir(fsys, name1, d1, walkDirFn); err != nil { + if err == SkipDir { + break + } + return err + } + } + return nil +} + +// WalkDir walks the file tree rooted at root, calling fn for each file or +// directory in the tree, including root. +// +// All errors that arise visiting files and directories are filtered by fn: +// see the [fs.WalkDirFunc] documentation for details. +// +// The files are walked in lexical order, which makes the output deterministic +// but requires WalkDir to read an entire directory into memory before proceeding +// to walk that directory. +// +// WalkDir does not follow symbolic links found in directories, +// but if root itself is a symbolic link, its target will be walked. +func WalkDir(fsys FS, root string, fn WalkDirFunc) error { + info, err := Stat(fsys, root) + if err != nil { + err = fn(root, nil, err) + } else { + err = walkDir(fsys, root, FileInfoToDirEntry(info), fn) + } + if err == SkipDir || err == SkipAll { + return nil + } + return err +} diff --git a/gnovm/stdlibs/testing/fstest/mapfs.gno b/gnovm/stdlibs/testing/fstest/mapfs.gno new file mode 100644 index 00000000000..d540379ddb9 --- /dev/null +++ b/gnovm/stdlibs/testing/fstest/mapfs.gno @@ -0,0 +1,259 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fstest + +import ( + "io" + "io/fs" + "path" + // "sort" + "strings" + "time" +) + +// A MapFS is a simple in-memory file system for use in tests, +// represented as a map from path names (arguments to Open) +// to information about the files or directories they represent. +// +// The map need not include parent directories for files contained +// in the map; those will be synthesized if needed. +// But a directory can still be included by setting the MapFile.Mode's [fs.ModeDir] bit; +// this may be necessary for detailed control over the directory's [fs.FileInfo] +// or to create an empty directory. +// +// File system operations read directly from the map, +// so that the file system can be changed by editing the map as needed. +// An implication is that file system operations must not run concurrently +// with changes to the map, which would be a race. +// Another implication is that opening or reading a directory requires +// iterating over the entire map, so a MapFS should typically be used with not more +// than a few hundred entries or directory reads. +type MapFS map[string]*MapFile + +// A MapFile describes a single file in a [MapFS]. +type MapFile struct { + Data []byte // file content + Mode fs.FileMode // fs.FileInfo.Mode + ModTime time.Time // fs.FileInfo.ModTime + Sys interface{} // fs.FileInfo.Sys +} + +var ( + _ fs.FS = MapFS(nil) + _ fs.File = (*openMapFile)(nil) +) + +// Open opens the named file. +func (fsys MapFS) Open(name string) (fs.File, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + file := fsys[name] + if file != nil && file.Mode&fs.ModeDir == 0 { + // Ordinary file + return &openMapFile{name, mapFileInfo{path.Base(name), file}, 0}, nil + } + + // Directory, possibly synthesized. + // Note that file can be nil here: the map need not contain explicit parent directories for all its files. + // But file can also be non-nil, in case the user wants to set metadata for the directory explicitly. + // Either way, we need to construct the list of children of this directory. + var list []mapFileInfo + var elem string + need := make(map[string]bool) + if name == "." { + elem = "." + for fname, f := range fsys { + i := strings.Index(fname, "/") + if i < 0 { + if fname != "." { + list = append(list, mapFileInfo{fname, f}) + } + } else { + need[fname[:i]] = true + } + } + } else { + elem = name[strings.LastIndex(name, "/")+1:] + prefix := name + "/" + for fname, f := range fsys { + if strings.HasPrefix(fname, prefix) { + felem := fname[len(prefix):] + i := strings.Index(felem, "/") + if i < 0 { + list = append(list, mapFileInfo{felem, f}) + } else { + need[fname[len(prefix):len(prefix)+i]] = true + } + } + } + // If the directory name is not in the map, + // and there are no children of the name in the map, + // then the directory is treated as not existing. + if file == nil && list == nil && len(need) == 0 { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + } + for _, fi := range list { + delete(need, fi.name) + } + for name := range need { + list = append(list, mapFileInfo{name, &MapFile{Mode: fs.ModeDir | 0o555}}) + } + + // XXX: sort.Slice is not supported yet. + // + // sort.Slice(list, func(i, j int) bool { + // return list[i].name < list[j].name + // }) + + // use custom sort to avoid dependency on sort package + // TODO: remove this once sort.Slice is supported + for i := 0; i < len(list); i++ { + for j := i + 1; j < len(list); j++ { + if list[i].name > list[j].name { + list[i], list[j] = list[j], list[i] + } + } + } + + if file == nil { + file = &MapFile{Mode: fs.ModeDir | 0o555} + } + return &mapDir{name, mapFileInfo{elem, file}, list, 0}, nil +} + +// fsOnly is a wrapper that hides all but the fs.FS methods, +// to avoid an infinite recursion when implementing special +// methods in terms of helpers that would use them. +// (In general, implementing these methods using the package fs helpers +// is redundant and unnecessary, but having the methods may make +// MapFS exercise more code paths when used in tests.) +type fsOnly struct{ fs.FS } + +func (fsys MapFS) ReadFile(name string) ([]byte, error) { + return fs.ReadFile(fsOnly{fsys}, name) +} + +func (fsys MapFS) Stat(name string) (fs.FileInfo, error) { + return fs.Stat(fsOnly{fsys}, name) +} + +func (fsys MapFS) ReadDir(name string) ([]fs.DirEntry, error) { + return fs.ReadDir(fsOnly{fsys}, name) +} + +func (fsys MapFS) Glob(pattern string) ([]string, error) { + return fs.Glob(fsOnly{fsys}, pattern) +} + +type noSub struct { + MapFS +} + +func (noSub) Sub() {} // not the fs.SubFS signature + +func (fsys MapFS) Sub(dir string) (fs.FS, error) { + return fs.Sub(noSub{fsys}, dir) +} + +// A mapFileInfo implements fs.FileInfo and fs.DirEntry for a given map file. +type mapFileInfo struct { + name string + f *MapFile +} + +func (i *mapFileInfo) Name() string { return i.name } +func (i *mapFileInfo) Size() int64 { return int64(len(i.f.Data)) } +func (i *mapFileInfo) Mode() fs.FileMode { return i.f.Mode } +func (i *mapFileInfo) Type() fs.FileMode { return i.f.Mode.Type() } +func (i *mapFileInfo) ModTime() time.Time { return i.f.ModTime } +func (i *mapFileInfo) IsDir() bool { return i.f.Mode&fs.ModeDir != 0 } +func (i *mapFileInfo) Sys() interface{} { return i.f.Sys } +func (i *mapFileInfo) Info() (fs.FileInfo, error) { return i, nil } + +func (i *mapFileInfo) String() string { + return fs.FormatFileInfo(i) +} + +// An openMapFile is a regular (non-directory) fs.File open for reading. +type openMapFile struct { + path string + mapFileInfo + offset int64 +} + +func (f *openMapFile) Stat() (fs.FileInfo, error) { return &f.mapFileInfo, nil } + +func (f *openMapFile) Close() error { return nil } + +func (f *openMapFile) Read(b []byte) (int, error) { + if f.offset >= int64(len(f.f.Data)) { + return 0, io.EOF + } + if f.offset < 0 { + return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid} + } + n := copy(b, f.f.Data[f.offset:]) + f.offset += int64(n) + return n, nil +} + +func (f *openMapFile) Seek(offset int64, whence int) (int64, error) { + switch whence { + case 0: + // offset += 0 + case 1: + offset += f.offset + case 2: + offset += int64(len(f.f.Data)) + } + if offset < 0 || offset > int64(len(f.f.Data)) { + return 0, &fs.PathError{Op: "seek", Path: f.path, Err: fs.ErrInvalid} + } + f.offset = offset + return offset, nil +} + +func (f *openMapFile) ReadAt(b []byte, offset int64) (int, error) { + if offset < 0 || offset > int64(len(f.f.Data)) { + return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid} + } + n := copy(b, f.f.Data[offset:]) + if n < len(b) { + return n, io.EOF + } + return n, nil +} + +// A mapDir is a directory fs.File (so also an fs.ReadDirFile) open for reading. +type mapDir struct { + path string + mapFileInfo + entry []mapFileInfo + offset int +} + +func (d *mapDir) Stat() (fs.FileInfo, error) { return &d.mapFileInfo, nil } +func (d *mapDir) Close() error { return nil } +func (d *mapDir) Read(b []byte) (int, error) { + return 0, &fs.PathError{Op: "read", Path: d.path, Err: fs.ErrInvalid} +} + +func (d *mapDir) ReadDir(count int) ([]fs.DirEntry, error) { + n := len(d.entry) - d.offset + if n == 0 && count > 0 { + return nil, io.EOF + } + if count > 0 && n > count { + n = count + } + list := make([]fs.DirEntry, n) + for i := range list { + list[i] = &d.entry[d.offset+i] + } + d.offset += n + return list, nil +} diff --git a/gnovm/stdlibs/testing/fstest/testfs.gno b/gnovm/stdlibs/testing/fstest/testfs.gno new file mode 100644 index 00000000000..aee3346d2a3 --- /dev/null +++ b/gnovm/stdlibs/testing/fstest/testfs.gno @@ -0,0 +1,636 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package fstest implements support for testing implementations and users of file systems. +package fstest + +import ( + "errors" + "fmt" + "io" + "io/fs" + "path" + // "reflect" + "sort" + "strings" + // "testing/iotest" +) + +// TestFS tests a file system implementation. +// It walks the entire tree of files in fsys, +// opening and checking that each file behaves correctly. +// It also checks that the file system contains at least the expected files. +// As a special case, if no expected files are listed, fsys must be empty. +// Otherwise, fsys must contain at least the listed files; it can also contain others. +// The contents of fsys must not change concurrently with TestFS. +// +// If TestFS finds any misbehaviors, it returns an error reporting all of them. +// The error text spans multiple lines, one per detected misbehavior. +// +// Typical usage inside a test is: +// +// if err := fstest.TestFS(myFS, "file/that/should/be/present"); err != nil { +// t.Fatal(err) +// } +// func TestFS(fsys fs.FS, expected ...string) error { +// if err := testFS(fsys, expected...); err != nil { +// return err +// } +// for _, name := range expected { +// if i := strings.Index(name, "/"); i >= 0 { +// dir, dirSlash := name[:i], name[:i+1] +// var subExpected []string +// for _, name := range expected { +// if strings.HasPrefix(name, dirSlash) { +// subExpected = append(subExpected, name[len(dirSlash):]) +// } +// } +// sub, err := fs.Sub(fsys, dir) +// if err != nil { +// return err +// } +// if err := testFS(sub, subExpected...); err != nil { +// return fmt.Errorf("testing fs.Sub(fsys, %s): %v", dir, err) +// } +// break // one sub-test is enough +// } +// } +// return nil +// } + +// func testFS(fsys fs.FS, expected ...string) error { +// t := fsTester{fsys: fsys} +// t.checkDir(".") +// t.checkOpen(".") +// found := make(map[string]bool) +// for _, dir := range t.dirs { +// found[dir] = true +// } +// for _, file := range t.files { +// found[file] = true +// } +// delete(found, ".") +// if len(expected) == 0 && len(found) > 0 { +// var list []string +// for k := range found { +// if k != "." { +// list = append(list, k) +// } +// } +// sort.Strings(list) +// if len(list) > 15 { +// list = append(list[:10], "...") +// } +// t.errorf("expected empty file system but found files:\n%s", strings.Join(list, "\n")) +// } +// for _, name := range expected { +// if !found[name] { +// t.errorf("expected but not found: %s", name) +// } +// } +// if len(t.errText) == 0 { +// return nil +// } +// return errors.New("TestFS found errors:\n" + string(t.errText)) +// } + +// An fsTester holds state for running the test. +type fsTester struct { + fsys fs.FS + errText []byte + dirs []string + files []string +} + +// errorf adds an error line to errText. +func (t *fsTester) errorf(format string, args ...interface{}) { + if len(t.errText) > 0 { + t.errText = append(t.errText, '\n') + } + t.errText = append(t.errText, fmt.Sprintf(format, args...)...) +} + +func (t *fsTester) openDir(dir string) fs.ReadDirFile { + f, err := t.fsys.Open(dir) + if err != nil { + t.errorf("%s: Open: %v", dir, err) + return nil + } + d, ok := f.(fs.ReadDirFile) + if !ok { + f.Close() + t.errorf("%s: Open returned File type %T, not a fs.ReadDirFile", dir, f) + return nil + } + return d +} + +// checkDir checks the directory dir, which is expected to exist +// (it is either the root or was found in a directory listing with IsDir true). +// func (t *fsTester) checkDir(dir string) { +// // Read entire directory. +// t.dirs = append(t.dirs, dir) +// d := t.openDir(dir) +// if d == nil { +// return +// } +// list, err := d.ReadDir(-1) +// if err != nil { +// d.Close() +// t.errorf("%s: ReadDir(-1): %v", dir, err) +// return +// } + +// // Check all children. +// var prefix string +// if dir == "." { +// prefix = "" +// } else { +// prefix = dir + "/" +// } +// for _, info := range list { +// name := info.Name() +// switch { +// case name == ".", name == "..", name == "": +// t.errorf("%s: ReadDir: child has invalid name: %#q", dir, name) +// continue +// case strings.Contains(name, "/"): +// t.errorf("%s: ReadDir: child name contains slash: %#q", dir, name) +// continue +// case strings.Contains(name, `\`): +// t.errorf("%s: ReadDir: child name contains backslash: %#q", dir, name) +// continue +// } +// path := prefix + name +// t.checkStat(path, info) +// t.checkOpen(path) +// if info.IsDir() { +// t.checkDir(path) +// } else { +// t.checkFile(path) +// } +// } + +// // Check ReadDir(-1) at EOF. +// list2, err := d.ReadDir(-1) +// if len(list2) > 0 || err != nil { +// d.Close() +// t.errorf("%s: ReadDir(-1) at EOF = %d entries, %v, wanted 0 entries, nil", dir, len(list2), err) +// return +// } + +// // Check ReadDir(1) at EOF (different results). +// list2, err = d.ReadDir(1) +// if len(list2) > 0 || err != io.EOF { +// d.Close() +// t.errorf("%s: ReadDir(1) at EOF = %d entries, %v, wanted 0 entries, EOF", dir, len(list2), err) +// return +// } + +// // Check that close does not report an error. +// if err := d.Close(); err != nil { +// t.errorf("%s: Close: %v", dir, err) +// } + +// // Check that closing twice doesn't crash. +// // The return value doesn't matter. +// d.Close() + +// // Reopen directory, read a second time, make sure contents match. +// if d = t.openDir(dir); d == nil { +// return +// } +// defer d.Close() +// list2, err = d.ReadDir(-1) +// if err != nil { +// t.errorf("%s: second Open+ReadDir(-1): %v", dir, err) +// return +// } +// t.checkDirList(dir, "first Open+ReadDir(-1) vs second Open+ReadDir(-1)", list, list2) + +// // Reopen directory, read a third time in pieces, make sure contents match. +// if d = t.openDir(dir); d == nil { +// return +// } +// defer d.Close() +// list2 = nil +// for { +// n := 1 +// if len(list2) > 0 { +// n = 2 +// } +// frag, err := d.ReadDir(n) +// if len(frag) > n { +// t.errorf("%s: third Open: ReadDir(%d) after %d: %d entries (too many)", dir, n, len(list2), len(frag)) +// return +// } +// list2 = append(list2, frag...) +// if err == io.EOF { +// break +// } +// if err != nil { +// t.errorf("%s: third Open: ReadDir(%d) after %d: %v", dir, n, len(list2), err) +// return +// } +// if n == 0 { +// t.errorf("%s: third Open: ReadDir(%d) after %d: 0 entries but nil error", dir, n, len(list2)) +// return +// } +// } +// t.checkDirList(dir, "first Open+ReadDir(-1) vs third Open+ReadDir(1,2) loop", list, list2) + +// // If fsys has ReadDir, check that it matches and is sorted. +// if fsys, ok := t.fsys.(fs.ReadDirFS); ok { +// list2, err := fsys.ReadDir(dir) +// if err != nil { +// t.errorf("%s: fsys.ReadDir: %v", dir, err) +// return +// } +// t.checkDirList(dir, "first Open+ReadDir(-1) vs fsys.ReadDir", list, list2) + +// for i := 0; i+1 < len(list2); i++ { +// if list2[i].Name() >= list2[i+1].Name() { +// t.errorf("%s: fsys.ReadDir: list not sorted: %s before %s", dir, list2[i].Name(), list2[i+1].Name()) +// } +// } +// } + +// // Check fs.ReadDir as well. +// list2, err = fs.ReadDir(t.fsys, dir) +// if err != nil { +// t.errorf("%s: fs.ReadDir: %v", dir, err) +// return +// } +// t.checkDirList(dir, "first Open+ReadDir(-1) vs fs.ReadDir", list, list2) + +// for i := 0; i+1 < len(list2); i++ { +// if list2[i].Name() >= list2[i+1].Name() { +// t.errorf("%s: fs.ReadDir: list not sorted: %s before %s", dir, list2[i].Name(), list2[i+1].Name()) +// } +// } + +// t.checkGlob(dir, list2) +// } + +// formatEntry formats an fs.DirEntry into a string for error messages and comparison. +func formatEntry(entry fs.DirEntry) string { + return fmt.Sprintf("%s IsDir=%v Type=%v", entry.Name(), entry.IsDir(), entry.Type()) +} + +// formatInfoEntry formats an fs.FileInfo into a string like the result of formatEntry, for error messages and comparison. +func formatInfoEntry(info fs.FileInfo) string { + return fmt.Sprintf("%s IsDir=%v Type=%v", info.Name(), info.IsDir(), info.Mode().Type()) +} + +// formatInfo formats an fs.FileInfo into a string for error messages and comparison. +func formatInfo(info fs.FileInfo) string { + return fmt.Sprintf("%s IsDir=%v Mode=%v Size=%d ModTime=%v", info.Name(), info.IsDir(), info.Mode(), info.Size(), info.ModTime()) +} + +// checkGlob checks that various glob patterns work if the file system implements GlobFS. +// func (t *fsTester) checkGlob(dir string, list []fs.DirEntry) { +// if _, ok := t.fsys.(fs.GlobFS); !ok { +// return +// } + +// // Make a complex glob pattern prefix that only matches dir. +// var glob string +// if dir != "." { +// elem := strings.Split(dir, "/") +// for i, e := range elem { +// var pattern []rune +// for j, r := range e { +// if r == '*' || r == '?' || r == '\\' || r == '[' || r == '-' { +// pattern = append(pattern, '\\', r) +// continue +// } +// switch (i + j) % 5 { +// case 0: +// pattern = append(pattern, r) +// case 1: +// pattern = append(pattern, '[', r, ']') +// case 2: +// pattern = append(pattern, '[', r, '-', r, ']') +// case 3: +// pattern = append(pattern, '[', '\\', r, ']') +// case 4: +// pattern = append(pattern, '[', '\\', r, '-', '\\', r, ']') +// } +// } +// elem[i] = string(pattern) +// } +// glob = strings.Join(elem, "/") + "/" +// } + +// // Test that malformed patterns are detected. +// // The error is likely path.ErrBadPattern but need not be. +// if _, err := t.fsys.(fs.GlobFS).Glob(glob + "nonexist/[]"); err == nil { +// t.errorf("%s: Glob(%#q): bad pattern not detected", dir, glob+"nonexist/[]") +// } + +// // Try to find a letter that appears in only some of the final names. +// c := rune('a') +// for ; c <= 'z'; c++ { +// have, haveNot := false, false +// for _, d := range list { +// if strings.ContainsRune(d.Name(), c) { +// have = true +// } else { +// haveNot = true +// } +// } +// if have && haveNot { +// break +// } +// } +// if c > 'z' { +// c = 'a' +// } +// glob += "*" + string(c) + "*" + +// var want []string +// for _, d := range list { +// if strings.ContainsRune(d.Name(), c) { +// want = append(want, path.Join(dir, d.Name())) +// } +// } + +// names, err := t.fsys.(fs.GlobFS).Glob(glob) +// if err != nil { +// t.errorf("%s: Glob(%#q): %v", dir, glob, err) +// return +// } +// if reflect.DeepEqual(want, names) { +// return +// } + +// if !sort.StringsAreSorted(names) { +// t.errorf("%s: Glob(%#q): unsorted output:\n%s", dir, glob, strings.Join(names, "\n")) +// sort.Strings(names) +// } + +// var problems []string +// for len(want) > 0 || len(names) > 0 { +// switch { +// case len(want) > 0 && len(names) > 0 && want[0] == names[0]: +// want, names = want[1:], names[1:] +// case len(want) > 0 && (len(names) == 0 || want[0] < names[0]): +// problems = append(problems, "missing: "+want[0]) +// want = want[1:] +// default: +// problems = append(problems, "extra: "+names[0]) +// names = names[1:] +// } +// } +// t.errorf("%s: Glob(%#q): wrong output:\n%s", dir, glob, strings.Join(problems, "\n")) +// } + +// checkStat checks that a direct stat of path matches entry, +// which was found in the parent's directory listing. +func (t *fsTester) checkStat(path string, entry fs.DirEntry) { + file, err := t.fsys.Open(path) + if err != nil { + t.errorf("%s: Open: %v", path, err) + return + } + info, err := file.Stat() + file.Close() + if err != nil { + t.errorf("%s: Stat: %v", path, err) + return + } + fentry := formatEntry(entry) + fientry := formatInfoEntry(info) + // Note: mismatch here is OK for symlink, because Open dereferences symlink. + if fentry != fientry && entry.Type()&fs.ModeSymlink == 0 { + t.errorf("%s: mismatch:\n\tentry = %s\n\tfile.Stat() = %s", path, fentry, fientry) + } + + einfo, err := entry.Info() + if err != nil { + t.errorf("%s: entry.Info: %v", path, err) + return + } + finfo := formatInfo(info) + if entry.Type()&fs.ModeSymlink != 0 { + // For symlink, just check that entry.Info matches entry on common fields. + // Open deferences symlink, so info itself may differ. + feentry := formatInfoEntry(einfo) + if fentry != feentry { + t.errorf("%s: mismatch\n\tentry = %s\n\tentry.Info() = %s\n", path, fentry, feentry) + } + } else { + feinfo := formatInfo(einfo) + if feinfo != finfo { + t.errorf("%s: mismatch:\n\tentry.Info() = %s\n\tfile.Stat() = %s\n", path, feinfo, finfo) + } + } + + // Stat should be the same as Open+Stat, even for symlinks. + info2, err := fs.Stat(t.fsys, path) + if err != nil { + t.errorf("%s: fs.Stat: %v", path, err) + return + } + finfo2 := formatInfo(info2) + if finfo2 != finfo { + t.errorf("%s: fs.Stat(...) = %s\n\twant %s", path, finfo2, finfo) + } + + if fsys, ok := t.fsys.(fs.StatFS); ok { + info2, err := fsys.Stat(path) + if err != nil { + t.errorf("%s: fsys.Stat: %v", path, err) + return + } + finfo2 := formatInfo(info2) + if finfo2 != finfo { + t.errorf("%s: fsys.Stat(...) = %s\n\twant %s", path, finfo2, finfo) + } + } +} + +// checkDirList checks that two directory lists contain the same files and file info. +// The order of the lists need not match. +func (t *fsTester) checkDirList(dir, desc string, list1, list2 []fs.DirEntry) { + old := make(map[string]fs.DirEntry) + checkMode := func(entry fs.DirEntry) { + if entry.IsDir() != (entry.Type()&fs.ModeDir != 0) { + if entry.IsDir() { + t.errorf("%s: ReadDir returned %s with IsDir() = true, Type() & ModeDir = 0", dir, entry.Name()) + } else { + t.errorf("%s: ReadDir returned %s with IsDir() = false, Type() & ModeDir = ModeDir", dir, entry.Name()) + } + } + } + + for _, entry1 := range list1 { + old[entry1.Name()] = entry1 + checkMode(entry1) + } + + var diffs []string + for _, entry2 := range list2 { + entry1 := old[entry2.Name()] + if entry1 == nil { + checkMode(entry2) + diffs = append(diffs, "+ "+formatEntry(entry2)) + continue + } + if formatEntry(entry1) != formatEntry(entry2) { + diffs = append(diffs, "- "+formatEntry(entry1), "+ "+formatEntry(entry2)) + } + delete(old, entry2.Name()) + } + for _, entry1 := range old { + diffs = append(diffs, "- "+formatEntry(entry1)) + } + + if len(diffs) == 0 { + return + } + + // XXX sort.Slice is not supported yet. + // + // sort.Slice(diffs, func(i, j int) bool { + // fi := strings.Fields(diffs[i]) + // fj := strings.Fields(diffs[j]) + // // sort by name (i < j) and then +/- (j < i, because + < -) + // return fi[1]+" "+fj[0] < fj[1]+" "+fi[0] + // }) + + for i := 0; i < len(diffs); i++ { + for j := i + 1; j < len(diffs); j++ { + fi := strings.Fields(diffs[i]) + fj := strings.Fields(diffs[j]) + if fi[1]+" "+fj[0] < fj[1]+" "+fi[0] { + diffs[i], diffs[j] = diffs[j], diffs[i] + } + } + } + + t.errorf("%s: diff %s:\n\t%s", dir, desc, strings.Join(diffs, "\n\t")) +} + +// checkFile checks that basic file reading works correctly. +func (t *fsTester) checkFile(file string) { + t.files = append(t.files, file) + + // Read entire file. + f, err := t.fsys.Open(file) + if err != nil { + t.errorf("%s: Open: %v", file, err) + return + } + + data, err := io.ReadAll(f) + if err != nil { + f.Close() + t.errorf("%s: Open+ReadAll: %v", file, err) + return + } + + if err := f.Close(); err != nil { + t.errorf("%s: Close: %v", file, err) + } + + // Check that closing twice doesn't crash. + // The return value doesn't matter. + f.Close() + + // Check that ReadFile works if present. + if fsys, ok := t.fsys.(fs.ReadFileFS); ok { + data2, err := fsys.ReadFile(file) + if err != nil { + t.errorf("%s: fsys.ReadFile: %v", file, err) + return + } + t.checkFileRead(file, "ReadAll vs fsys.ReadFile", data, data2) + + // Modify the data and check it again. Modifying the + // returned byte slice should not affect the next call. + for i := range data2 { + data2[i]++ + } + data2, err = fsys.ReadFile(file) + if err != nil { + t.errorf("%s: second call to fsys.ReadFile: %v", file, err) + return + } + t.checkFileRead(file, "Readall vs second fsys.ReadFile", data, data2) + + t.checkBadPath(file, "ReadFile", + func(name string) error { _, err := fsys.ReadFile(name); return err }) + } + + // Check that fs.ReadFile works with t.fsys. + data2, err := fs.ReadFile(t.fsys, file) + if err != nil { + t.errorf("%s: fs.ReadFile: %v", file, err) + return + } + t.checkFileRead(file, "ReadAll vs fs.ReadFile", data, data2) + + // Use iotest.TestReader to check small reads, Seek, ReadAt. + f, err = t.fsys.Open(file) + if err != nil { + t.errorf("%s: second Open: %v", file, err) + return + } + defer f.Close() + // if err := iotest.TestReader(f, data); err != nil { + // t.errorf("%s: failed TestReader:\n\t%s", file, strings.ReplaceAll(err.Error(), "\n", "\n\t")) + // } +} + +func (t *fsTester) checkFileRead(file, desc string, data1, data2 []byte) { + if string(data1) != string(data2) { + t.errorf("%s: %s: different data returned\n\t%q\n\t%q", file, desc, data1, data2) + return + } +} + +// checkBadPath checks that various invalid forms of file's name cannot be opened using t.fsys.Open. +func (t *fsTester) checkOpen(file string) { + t.checkBadPath(file, "Open", func(file string) error { + f, err := t.fsys.Open(file) + if err == nil { + f.Close() + } + return err + }) +} + +// checkBadPath checks that various invalid forms of file's name cannot be opened using open. +func (t *fsTester) checkBadPath(file string, desc string, open func(string) error) { + bad := []string{ + "/" + file, + file + "/.", + } + if file == "." { + bad = append(bad, "/") + } + if i := strings.Index(file, "/"); i >= 0 { + bad = append(bad, + file[:i]+"//"+file[i+1:], + file[:i]+"/./"+file[i+1:], + file[:i]+`\`+file[i+1:], + file[:i]+"/../"+file, + ) + } + if i := strings.LastIndex(file, "/"); i >= 0 { + bad = append(bad, + file[:i]+"//"+file[i+1:], + file[:i]+"/./"+file[i+1:], + file[:i]+`\`+file[i+1:], + file+"/../"+file[i+1:], + ) + } + + for _, b := range bad { + if err := open(b); err == nil { + t.errorf("%s: %s(%s) succeeded, want error", file, desc, b) + } + } +} diff --git a/gnovm/stdlibs/time/format.gno b/gnovm/stdlibs/time/format.gno index 8431ff89b45..61a9eb3301b 100644 --- a/gnovm/stdlibs/time/format.gno +++ b/gnovm/stdlibs/time/format.gno @@ -116,6 +116,9 @@ const ( StampMilli = "Jan _2 15:04:05.000" StampMicro = "Jan _2 15:04:05.000000" StampNano = "Jan _2 15:04:05.000000000" + DateTime = "2006-01-02 15:04:05" + DateOnly = "2006-01-02" + TimeOnly = "15:04:05" ) const (