From 6bcb40fe01d83451eafe42a195c4acf1b186b2dd Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 30 Mar 2020 20:52:52 +0200 Subject: [PATCH] os: implement virtual filesystem support This allows applications to mount filesystems in the os package. This is useful for mounting external flash filesystems, for example. --- src/os/file.go | 157 +++++++++++++++++++++------------- src/os/file_other.go | 32 +++++-- src/os/file_unix.go | 98 +++++++++++++++++++-- src/os/filesystem.go | 85 ++++++++++++++++++ src/syscall/syscall_darwin.go | 39 +++++---- src/syscall/syscall_libc.go | 8 ++ testdata/stdlib.go | 6 +- testdata/stdlib.txt | 6 +- 8 files changed, 336 insertions(+), 95 deletions(-) create mode 100644 src/os/filesystem.go diff --git a/src/os/file.go b/src/os/file.go index de971fb7..6f4a8cc1 100644 --- a/src/os/file.go +++ b/src/os/file.go @@ -10,49 +10,119 @@ import ( ) // Portable analogs of some common system call errors. +// Note that these are exported for use in the Filesystem interface. var ( - errUnsupported = errors.New("operation not supported") - notImplemented = errors.New("os: not implemented") + ErrUnsupported = errors.New("operation not supported") + ErrNotImplemented = errors.New("operation not implemented") + ErrNotExist = errors.New("file not found") + ErrExist = errors.New("file exists") ) -// Stdin, Stdout, and Stderr are open Files pointing to the standard input, -// standard output, and standard error file descriptors. -var ( - Stdin = &File{0, "/dev/stdin"} - Stdout = &File{1, "/dev/stdout"} - Stderr = &File{2, "/dev/stderr"} -) +// Mkdir creates a directory. If the operation fails, it will return an error of +// type *PathError. +func Mkdir(path string, perm FileMode) error { + fs, suffix := findMount(path) + if fs == nil { + return &PathError{"mkdir", path, ErrNotExist} + } + err := fs.Mkdir(suffix, perm) + if err != nil { + return &PathError{"mkdir", path, err} + } + return nil +} + +// Remove removes a file or (empty) directory. If the operation fails, it will +// return an error of type *PathError. +func Remove(path string) error { + fs, suffix := findMount(path) + if fs == nil { + return &PathError{"remove", path, ErrNotExist} + } + err := fs.Remove(suffix) + if err != nil { + return &PathError{"remove", path, err} + } + return nil +} // File represents an open file descriptor. type File struct { - fd uintptr - name string + handle FileHandle + name string +} + +// Name returns the name of the file with which it was opened. +func (f *File) Name() string { + return f.name +} + +// OpenFile opens the named file. If the operation fails, the returned error +// will be of type *PathError. +func OpenFile(name string, flag int, perm FileMode) (*File, error) { + fs, suffix := findMount(name) + if fs == nil { + return nil, &PathError{"open", name, ErrNotExist} + } + handle, err := fs.OpenFile(suffix, flag, perm) + if err != nil { + return nil, &PathError{"open", name, err} + } + return &File{name: name, handle: handle}, nil +} + +// Open opens the file named for reading. +func Open(name string) (*File, error) { + return OpenFile(name, O_RDONLY, 0) +} + +// Create creates the named file, overwriting it if it already exists. +func Create(name string) (*File, error) { + return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666) +} + +// Read reads up to len(b) bytes from the File. It returns the number of bytes +// read and any error encountered. At end of file, Read returns 0, io.EOF. +func (f *File) Read(b []byte) (n int, err error) { + n, err = f.handle.Read(b) + if err != nil { + err = &PathError{"read", f.name, err} + } + return +} + +// Write writes len(b) bytes to the File. It returns the number of bytes written +// and an error, if any. Write returns a non-nil error when n != len(b). +func (f *File) Write(b []byte) (n int, err error) { + n, err = f.handle.Write(b) + if err != nil { + err = &PathError{"write", f.name, err} + } + return +} + +// Close closes the File, rendering it unusable for I/O. +func (f *File) Close() (err error) { + err = f.handle.Close() + if err != nil { + err = &PathError{"close", f.name, err} + } + return } // Readdir is a stub, not yet implemented func (f *File) Readdir(n int) ([]FileInfo, error) { - return nil, notImplemented + return nil, &PathError{"readdir", f.name, ErrNotImplemented} } // Readdirnames is a stub, not yet implemented func (f *File) Readdirnames(n int) (names []string, err error) { - return nil, notImplemented + return nil, &PathError{"readdirnames", f.name, ErrNotImplemented} } // Stat is a stub, not yet implemented func (f *File) Stat() (FileInfo, error) { - return nil, notImplemented -} - -// NewFile returns a new File with the given file descriptor and name. -func NewFile(fd uintptr, name string) *File { - return &File{fd, name} -} - -// Fd returns the integer Unix file descriptor referencing the open file. The -// file descriptor is valid only until f.Close is called. -func (f *File) Fd() uintptr { - return f.fd + return nil, &PathError{"stat", f.name, ErrNotImplemented} } const ( @@ -72,32 +142,8 @@ type PathError struct { Err error } -func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() } - -// Open is a super simple stub function (for now), only capable of opening stdin, stdout, and stderr -func Open(name string) (*File, error) { - fd := uintptr(999) - switch name { - case "/dev/stdin": - fd = 0 - case "/dev/stdout": - fd = 1 - case "/dev/stderr": - fd = 2 - default: - return nil, &PathError{"open", name, notImplemented} - } - return &File{fd, name}, nil -} - -// OpenFile is a stub, passing through to the stub Open() call -func OpenFile(name string, flag int, perm FileMode) (*File, error) { - return Open(name) -} - -// Create is a stub, passing through to the stub Open() call -func Create(name string) (*File, error) { - return Open(name) +func (e *PathError) Error() string { + return e.Op + " " + e.Path + ": " + e.Err.Error() } type FileMode uint32 @@ -155,12 +201,12 @@ type FileInfo interface { // Stat is a stub, not yet implemented func Stat(name string) (FileInfo, error) { - return nil, notImplemented + return nil, &PathError{"stat", name, ErrNotImplemented} } // Lstat is a stub, not yet implemented func Lstat(name string) (FileInfo, error) { - return nil, notImplemented + return nil, &PathError{"lstat", name, ErrNotImplemented} } // Getwd is a stub (for now), always returning an empty string @@ -178,11 +224,6 @@ func TempDir() string { return "/tmp" } -// Mkdir is a stub, not yet implemented -func Mkdir(name string, perm FileMode) error { - return notImplemented -} - // IsExist is a stub (for now), always returning false func IsExist(err error) bool { return false diff --git a/src/os/file_other.go b/src/os/file_other.go index 50bfa226..be98cc69 100644 --- a/src/os/file_other.go +++ b/src/os/file_other.go @@ -6,28 +6,44 @@ import ( _ "unsafe" ) +// Stdin, Stdout, and Stderr are open Files pointing to the standard input, +// standard output, and standard error file descriptors. +var ( + Stdin = &File{stdioFileHandle(0), "/dev/stdin"} + Stdout = &File{stdioFileHandle(1), "/dev/stdout"} + Stderr = &File{stdioFileHandle(2), "/dev/stderr"} +) + +// isOS indicates whether we're running on a real operating system with +// filesystem support. +const isOS = false + +// stdioFileHandle represents one of stdin, stdout, or stderr depending on the +// number. It implements the FileHandle interface. +type stdioFileHandle uint8 + // Read is unsupported on this system. -func (f *File) Read(b []byte) (n int, err error) { - return 0, errUnsupported +func (f stdioFileHandle) Read(b []byte) (n int, err error) { + return 0, ErrUnsupported } // Write writes len(b) bytes to the output. It returns the number of bytes // written or an error if this file is not stdout or stderr. -func (f *File) Write(b []byte) (n int, err error) { - switch f.fd { - case Stdout.fd, Stderr.fd: +func (f stdioFileHandle) Write(b []byte) (n int, err error) { + switch f { + case 1, 2: // stdout, stderr for _, c := range b { putchar(c) } return len(b), nil default: - return 0, errUnsupported + return 0, ErrUnsupported } } // Close is unsupported on this system. -func (f *File) Close() error { - return errUnsupported +func (f stdioFileHandle) Close() error { + return ErrUnsupported } //go:linkname putchar runtime.putchar diff --git a/src/os/file_unix.go b/src/os/file_unix.go index 9f2a31e9..e77d6773 100644 --- a/src/os/file_unix.go +++ b/src/os/file_unix.go @@ -6,19 +6,105 @@ import ( "syscall" ) +func init() { + // Mount the host filesystem at the root directory. This is what most + // programs will be expecting. + Mount("/", unixFilesystem{}) +} + +// Stdin, Stdout, and Stderr are open Files pointing to the standard input, +// standard output, and standard error file descriptors. +var ( + Stdin = &File{unixFileHandle(0), "/dev/stdin"} + Stdout = &File{unixFileHandle(1), "/dev/stdout"} + Stderr = &File{unixFileHandle(2), "/dev/stderr"} +) + +// isOS indicates whether we're running on a real operating system with +// filesystem support. +const isOS = true + +// unixFilesystem is an empty handle for a Unix/Linux filesystem. All operations +// are relative to the current working directory. +type unixFilesystem struct { +} + +func (fs unixFilesystem) Mkdir(path string, perm FileMode) error { + return handleSyscallError(syscall.Mkdir(path, uint32(perm))) +} + +func (fs unixFilesystem) Remove(path string) error { + return handleSyscallError(syscall.Unlink(path)) +} + +func (fs unixFilesystem) OpenFile(path string, flag int, perm FileMode) (FileHandle, error) { + // Map os package flags to syscall flags. + syscallFlag := 0 + if flag&O_RDONLY != 0 { + syscallFlag |= syscall.O_RDONLY + } + if flag&O_WRONLY != 0 { + syscallFlag |= syscall.O_WRONLY + } + if flag&O_RDWR != 0 { + syscallFlag |= syscall.O_RDWR + } + if flag&O_APPEND != 0 { + syscallFlag |= syscall.O_APPEND + } + if flag&O_CREATE != 0 { + syscallFlag |= syscall.O_CREAT + } + if flag&O_EXCL != 0 { + syscallFlag |= syscall.O_EXCL + } + if flag&O_SYNC != 0 { + syscallFlag |= syscall.O_SYNC + } + if flag&O_TRUNC != 0 { + syscallFlag |= syscall.O_TRUNC + } + fp, err := syscall.Open(path, syscallFlag, uint32(perm)) + return unixFileHandle(fp), handleSyscallError(err) +} + +// unixFileHandle is a Unix file pointer with associated methods that implement +// the FileHandle interface. +type unixFileHandle uintptr + // Read reads up to len(b) bytes from the File. It returns the number of bytes // read and any error encountered. At end of file, Read returns 0, io.EOF. -func (f *File) Read(b []byte) (n int, err error) { - return syscall.Read(int(f.fd), b) +func (f unixFileHandle) Read(b []byte) (n int, err error) { + n, err = syscall.Read(int(f), b) + err = handleSyscallError(err) + return } // Write writes len(b) bytes to the File. It returns the number of bytes written // and an error, if any. Write returns a non-nil error when n != len(b). -func (f *File) Write(b []byte) (n int, err error) { - return syscall.Write(int(f.fd), b) +func (f unixFileHandle) Write(b []byte) (n int, err error) { + n, err = syscall.Write(int(f), b) + err = handleSyscallError(err) + return } // Close closes the File, rendering it unusable for I/O. -func (f *File) Close() error { - return syscall.Close(int(f.fd)) +func (f unixFileHandle) Close() error { + return handleSyscallError(syscall.Close(int(f))) +} + +// handleSyscallError converts syscall errors into regular os package errors. +// The err parameter must be either nil or of type syscall.Errno. +func handleSyscallError(err error) error { + if err == nil { + return nil + } + switch err.(syscall.Errno) { + case syscall.EEXIST: + return ErrExist + case syscall.ENOENT: + return ErrNotExist + default: + return err + } } diff --git a/src/os/filesystem.go b/src/os/filesystem.go new file mode 100644 index 00000000..b0719c6c --- /dev/null +++ b/src/os/filesystem.go @@ -0,0 +1,85 @@ +package os + +import ( + "strings" +) + +// mounts lists the mount points currently mounted in the filesystem provided by +// the os package. To resolve a path to a mount point, it is scanned from top to +// bottom looking for the first prefix match. +var mounts []mountPoint + +type mountPoint struct { + // prefix is a filesystem prefix, that always starts and ends with a forward + // slash. To denote the root filesystem, use a single slash: "/". + // This allows fast checking whether a path lies within a mount point. + prefix string + + // filesystem is the Filesystem implementation that is mounted at this mount + // point. + filesystem Filesystem +} + +// Filesystem provides an interface for generic filesystem drivers mounted in +// the os package. The errors returned must be one of the os.Err* errors, or a +// custom error if one doesn't exist. It should not be a *PathError because +// errors will be wrapped with a *PathError by the filesystem abstraction. +// +// WARNING: this interface is not finalized and may change in a future version. +type Filesystem interface { + // OpenFile opens the named file. + OpenFile(name string, flag int, perm FileMode) (FileHandle, error) + + // Mkdir creates a new directoy with the specified permission (before + // umask). Some filesystems may not support directories or permissions. + Mkdir(name string, perm FileMode) error + + // Remove removes the named file or (empty) directory. + Remove(name string) error +} + +// FileHandle is an interface that should be implemented by filesystems +// implementing the Filesystem interface. +// +// WARNING: this interface is not finalized and may change in a future version. +type FileHandle interface { + // Read reads up to len(b) bytes from the file. + Read(b []byte) (n int, err error) + + // Write writes up to len(b) bytes to the file. + Write(b []byte) (n int, err error) + + // Close closes the file, making it unusable for further writes. + Close() (err error) +} + +// findMount returns the appropriate (mounted) filesystem to use for a given +// filename plus the path relative to that filesystem. +func findMount(path string) (Filesystem, string) { + for i := len(mounts) - 1; i >= 0; i-- { + mount := mounts[i] + if strings.HasPrefix(path, mount.prefix) { + return mount.filesystem, path[len(mount.prefix)-1:] + } + } + if isOS { + // Assume that the first entry in the mounts slice is the OS filesystem + // at the root of the directory tree. Use it as-is, to support relative + // paths. + return mounts[0].filesystem, path + } + return nil, path +} + +// Mount mounts the given filesystem in the filesystem abstraction layer of the +// os package. It is not possible to unmount filesystems. Filesystems added +// later will override earlier filesystems. +// +// The provided prefix must start and end with a forward slash. This is true for +// the root directory ("/") for example. +func Mount(prefix string, filesystem Filesystem) { + if prefix[0] != '/' || prefix[len(prefix)-1] != '/' { + panic("os.Mount: invalid prefix") + } + mounts = append(mounts, mountPoint{prefix, filesystem}) +} diff --git a/src/syscall/syscall_darwin.go b/src/syscall/syscall_darwin.go index 176fe61d..e2e7f408 100644 --- a/src/syscall/syscall_darwin.go +++ b/src/syscall/syscall_darwin.go @@ -1,8 +1,7 @@ package syscall // This file defines errno and constants to match the darwin libsystem ABI. -// Values have been determined experimentally by compiling some C code on macOS -// with Clang and looking at the resulting LLVM IR. +// Values have been copied from src/syscall/zerrors_darwin_amd64.go. // This function returns the error location in the darwin ABI. // Discovered by compiling the following code using Clang: @@ -24,28 +23,34 @@ func getErrno() Errno { } const ( - ENOENT Errno = 2 - EINTR Errno = 4 - EMFILE Errno = 24 - EAGAIN Errno = 35 - ETIMEDOUT Errno = 60 - ENOSYS Errno = 78 + ENOENT Errno = 0x2 + EEXIST Errno = 0x11 + EINTR Errno = 0x4 + EMFILE Errno = 0x18 + EAGAIN Errno = 0x23 + ETIMEDOUT Errno = 0x3c + ENOSYS Errno = 0x4e EWOULDBLOCK Errno = EAGAIN ) type Signal int const ( - SIGCHLD Signal = 20 - SIGINT Signal = 2 - SIGKILL Signal = 9 - SIGTRAP Signal = 5 - SIGQUIT Signal = 3 - SIGTERM Signal = 15 + SIGCHLD Signal = 0x14 + SIGINT Signal = 0x2 + SIGKILL Signal = 0x9 + SIGTRAP Signal = 0x5 + SIGQUIT Signal = 0x3 + SIGTERM Signal = 0xf ) const ( - O_RDONLY = 0 - O_WRONLY = 1 - O_RDWR = 2 + O_RDONLY = 0x0 + O_WRONLY = 0x1 + O_RDWR = 0x2 + O_APPEND = 0x8 + O_SYNC = 0x80 + O_CREAT = 0x200 + O_TRUNC = 0x400 + O_EXCL = 0x800 ) diff --git a/src/syscall/syscall_libc.go b/src/syscall/syscall_libc.go index 78388804..3f24ddb7 100644 --- a/src/syscall/syscall_libc.go +++ b/src/syscall/syscall_libc.go @@ -31,6 +31,14 @@ func Open(path string, mode int, perm uint32) (fd int, err error) { return 0, ENOSYS // TODO } +func Mkdir(path string, mode uint32) (err error) { + return ENOSYS // TODO +} + +func Unlink(path string) (err error) { + return ENOSYS // TODO +} + func Kill(pid int, sig Signal) (err error) { return ENOSYS // TODO } diff --git a/testdata/stdlib.go b/testdata/stdlib.go index b0f2ce39..b380e803 100644 --- a/testdata/stdlib.go +++ b/testdata/stdlib.go @@ -9,9 +9,9 @@ import ( func main() { // package os, fmt - fmt.Println("stdin: ", os.Stdin.Fd()) - fmt.Println("stdout:", os.Stdout.Fd()) - fmt.Println("stderr:", os.Stderr.Fd()) + fmt.Println("stdin: ", os.Stdin.Name()) + fmt.Println("stdout:", os.Stdout.Name()) + fmt.Println("stderr:", os.Stderr.Name()) // package math/rand fmt.Println("pseudorandom number:", rand.Int31()) diff --git a/testdata/stdlib.txt b/testdata/stdlib.txt index ecf4402d..1862d0d2 100644 --- a/testdata/stdlib.txt +++ b/testdata/stdlib.txt @@ -1,6 +1,6 @@ -stdin: 0 -stdout: 1 -stderr: 2 +stdin: /dev/stdin +stdout: /dev/stdout +stderr: /dev/stderr pseudorandom number: 1298498081 strings.IndexByte: 2 strings.Replace: An-example-string