From 602f35a5ca10093639ecad8c875848df48c3b311 Mon Sep 17 00:00:00 2001 From: Achille Date: Fri, 5 May 2023 00:40:15 -0700 Subject: [PATCH] os: implement os.(*File).WriteAt (#3697) os: implement os.(*File).WriteAt Signed-off-by: Achille Roussel --- src/os/export_test.go | 1 + src/os/file.go | 37 +++++++++++++++++---- src/os/file_other.go | 11 +++++-- src/os/file_unix.go | 21 +++++++++--- src/os/file_windows.go | 18 +++++++++-- src/os/filesystem.go | 3 ++ src/os/os_test.go | 64 +++++++++++++++++++++++++++++++++++++ src/syscall/syscall_libc.go | 14 ++++++++ 8 files changed, 152 insertions(+), 17 deletions(-) diff --git a/src/os/export_test.go b/src/os/export_test.go index 7a218ddf..e1898d0f 100644 --- a/src/os/export_test.go +++ b/src/os/export_test.go @@ -6,4 +6,5 @@ package os // Export for testing. +var ErrWriteAtInAppendMode = errWriteAtInAppendMode var ErrPatternHasSeparator = errPatternHasSeparator diff --git a/src/os/file.go b/src/os/file.go index 153d8a02..ff62899b 100644 --- a/src/os/file.go +++ b/src/os/file.go @@ -90,7 +90,9 @@ func OpenFile(name string, flag int, perm FileMode) (*File, error) { if err != nil { return nil, &PathError{Op: "open", Path: name, Err: err} } - return NewFile(handle, name), nil + f := NewFile(handle, name) + f.appendMode = (flag & O_APPEND) != 0 + return f, nil } // Open opens the file named for reading. @@ -170,13 +172,34 @@ func (f *File) WriteString(s string) (n int, err error) { return f.Write([]byte(s)) } -func (f *File) WriteAt(b []byte, off int64) (n int, err error) { - if f.handle == nil { - err = ErrClosed - } else { - err = ErrNotImplemented +var errWriteAtInAppendMode = errors.New("os: invalid use of WriteAt on file opened with O_APPEND") + +// WriteAt writes len(b) bytes to the File starting at byte offset off. +// It returns the number of bytes written and an error, if any. +// WriteAt returns a non-nil error when n != len(b). +// +// If file was opened with the O_APPEND flag, WriteAt returns an error. +func (f *File) WriteAt(b []byte, offset int64) (n int, err error) { + switch { + case offset < 0: + return 0, &PathError{Op: "writeat", Path: f.name, Err: errNegativeOffset} + case f.handle == nil: + return 0, &PathError{Op: "writeat", Path: f.name, Err: ErrClosed} + case f.appendMode: + // Go does not wrap this error but it would be more consistent + // if it did. + return 0, errWriteAtInAppendMode + } + for len(b) > 0 { + m, e := f.handle.WriteAt(b, offset) + if e != nil { + err = &PathError{Op: "writeat", Path: f.name, Err: e} + break + } + n += m + b = b[m:] + offset += int64(m) } - err = &PathError{Op: "writeat", Path: f.name, Err: err} return } diff --git a/src/os/file_other.go b/src/os/file_other.go index 4b8d0b76..7e5833d3 100644 --- a/src/os/file_other.go +++ b/src/os/file_other.go @@ -29,8 +29,9 @@ type stdioFileHandle uint8 // can overwrite this data, which could cause the finalizer // to close the wrong file descriptor. type file struct { - handle FileHandle - name string + handle FileHandle + name string + appendMode bool } func (f *file) close() error { @@ -38,7 +39,7 @@ func (f *file) close() error { } func NewFile(fd uintptr, name string) *File { - return &File{&file{stdioFileHandle(fd), name}} + return &File{&file{handle: stdioFileHandle(fd), name: name}} } // Read reads up to len(b) bytes from machine.Serial. @@ -67,6 +68,10 @@ func (f stdioFileHandle) ReadAt(b []byte, off int64) (n int, err error) { return 0, ErrNotImplemented } +func (f stdioFileHandle) WriteAt(b []byte, off int64) (n int, err error) { + return 0, ErrNotImplemented +} + // 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 stdioFileHandle) Write(b []byte) (n int, err error) { diff --git a/src/os/file_unix.go b/src/os/file_unix.go index fd31e530..6314db8f 100644 --- a/src/os/file_unix.go +++ b/src/os/file_unix.go @@ -37,9 +37,10 @@ func rename(oldname, newname string) error { // can overwrite this data, which could cause the finalizer // to close the wrong file descriptor. type file struct { - handle FileHandle - name string - dirinfo *dirInfo // nil unless directory being read + handle FileHandle + name string + dirinfo *dirInfo // nil unless directory being read + appendMode bool } func (f *file) close() (err error) { @@ -51,7 +52,7 @@ func (f *file) close() (err error) { } func NewFile(fd uintptr, name string) *File { - return &File{&file{unixFileHandle(fd), name, nil}} + return &File{&file{handle: unixFileHandle(fd), name: name}} } func Pipe() (r *File, w *File, err error) { @@ -124,6 +125,18 @@ func (f unixFileHandle) ReadAt(b []byte, offset int64) (n int, err error) { return } +// WriteAt writes len(b) bytes to the File starting at byte offset off. +// It returns the number of bytes written and an error, if any. +// WriteAt returns a non-nil error when n != len(b). +// +// If file was opened with the O_APPEND flag, WriteAt returns an error. +// +// TODO: move to file_anyos once WriteAt is implemented for windows. +func (f unixFileHandle) WriteAt(b []byte, offset int64) (int, error) { + n, err := syscall.Pwrite(syscallFd(f), b, offset) + return n, handleSyscallError(err) +} + // Seek wraps syscall.Seek. func (f unixFileHandle) Seek(offset int64, whence int) (int64, error) { newoffset, err := syscall.Seek(syscallFd(f), offset, whence) diff --git a/src/os/file_windows.go b/src/os/file_windows.go index f882ea21..70f3a3dd 100644 --- a/src/os/file_windows.go +++ b/src/os/file_windows.go @@ -35,8 +35,9 @@ func rename(oldname, newname string) error { } type file struct { - handle FileHandle - name string + handle FileHandle + name string + appendMode bool } func (f *file) close() error { @@ -44,7 +45,7 @@ func (f *file) close() error { } func NewFile(fd uintptr, name string) *File { - return &File{&file{unixFileHandle(fd), name}} + return &File{&file{handle: unixFileHandle(fd), name: name}} } func Pipe() (r *File, w *File, err error) { @@ -84,6 +85,17 @@ func (f unixFileHandle) ReadAt(b []byte, offset int64) (n int, err error) { return -1, ErrNotImplemented } +// WriteAt writes len(b) bytes to the File starting at byte offset off. +// It returns the number of bytes written and an error, if any. +// WriteAt returns a non-nil error when n != len(b). +// +// If file was opened with the O_APPEND flag, WriteAt returns an error. +// +// TODO: move to file_anyos once WriteAt is implemented for windows. +func (f unixFileHandle) WriteAt(b []byte, offset int64) (n int, err error) { + return -1, ErrNotImplemented +} + // Seek wraps syscall.Seek. func (f unixFileHandle) Seek(offset int64, whence int) (int64, error) { newoffset, err := syscall.Seek(syscallFd(f), offset, whence) diff --git a/src/os/filesystem.go b/src/os/filesystem.go index c14a1021..f38b2266 100644 --- a/src/os/filesystem.go +++ b/src/os/filesystem.go @@ -58,6 +58,9 @@ type FileHandle interface { // Write writes up to len(b) bytes to the file. Write(b []byte) (n int, err error) + // WriteAt writes b to the file at the given absolute offset + WriteAt(b []byte, offset int64) (n int, err error) + // Close closes the file, making it unusable for further writes. Close() (err error) } diff --git a/src/os/os_test.go b/src/os/os_test.go index d79ad4a3..58044c16 100644 --- a/src/os/os_test.go +++ b/src/os/os_test.go @@ -5,6 +5,7 @@ package os_test import ( + "fmt" "io" "os" . "os" @@ -249,3 +250,66 @@ func TestReadAtEOF(t *testing.T) { t.Fatalf("ReadAt failed: %s", err) } } + +func TestWriteAt(t *testing.T) { + if runtime.GOOS == "windows" { + t.Log("TODO: implement Pwrite for Windows") + return + } + f := newFile("TestWriteAt", t) + defer Remove(f.Name()) + defer f.Close() + + const data = "hello, world\n" + io.WriteString(f, data) + + n, err := f.WriteAt([]byte("WORLD"), 7) + if err != nil || n != 5 { + t.Fatalf("WriteAt 7: %d, %v", n, err) + } + + b, err := os.ReadFile(f.Name()) + if err != nil { + t.Fatalf("ReadFile %s: %v", f.Name(), err) + } + if string(b) != "hello, WORLD\n" { + t.Fatalf("after write: have %q want %q", string(b), "hello, WORLD\n") + } +} + +// Verify that WriteAt doesn't allow negative offset. +func TestWriteAtNegativeOffset(t *testing.T) { + if runtime.GOOS == "windows" { + t.Log("TODO: implement Pwrite for Windows") + return + } + f := newFile("TestWriteAtNegativeOffset", t) + defer Remove(f.Name()) + defer f.Close() + + n, err := f.WriteAt([]byte("WORLD"), -10) + + const wantsub = "negative offset" + if !strings.Contains(fmt.Sprint(err), wantsub) || n != 0 { + t.Errorf("WriteAt(-10) = %v, %v; want 0, ...%q...", n, err, wantsub) + } +} + +// Verify that WriteAt doesn't work in append mode. +func TestWriteAtInAppendMode(t *testing.T) { + if runtime.GOOS == "windows" { + t.Log("TODO: implement Pwrite for Windows") + return + } + defer chtmpdir(t)() + f, err := OpenFile("write_at_in_append_mode.txt", O_APPEND|O_CREATE|O_WRONLY, 0666) + if err != nil { + t.Fatalf("OpenFile: %v", err) + } + defer f.Close() + + _, err = f.WriteAt([]byte(""), 1) + if err != ErrWriteAtInAppendMode { + t.Fatalf("f.WriteAt returned %v, expected %v", err, ErrWriteAtInAppendMode) + } +} diff --git a/src/syscall/syscall_libc.go b/src/syscall/syscall_libc.go index aaba7837..ea0a000d 100644 --- a/src/syscall/syscall_libc.go +++ b/src/syscall/syscall_libc.go @@ -54,6 +54,15 @@ func Pread(fd int, p []byte, offset int64) (n int, err error) { return } +func Pwrite(fd int, p []byte, offset int64) (n int, err error) { + buf, count := splitSlice(p) + n = libc_pwrite(int32(fd), buf, uint(count), offset) + if n < 0 { + err = getErrno() + } + return +} + func Seek(fd int, offset int64, whence int) (newoffset int64, err error) { newoffset = libc_lseek(int32(fd), offset, whence) if newoffset < 0 { @@ -342,6 +351,11 @@ func libc_read(fd int32, buf *byte, count uint) int //export pread func libc_pread(fd int32, buf *byte, count uint, offset int64) int +// ssize_t pwrite(int fd, void *buf, size_t count, off_t offset); +// +//export pwrite +func libc_pwrite(fd int32, buf *byte, count uint, offset int64) int + // ssize_t lseek(int fd, off_t offset, int whence); // //export lseek