From ccd992355df7192993c666236047820244914598 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 16 Apr 2024 21:19:13 +0200 Subject: Adding upstream version 1.21.8. Signed-off-by: Daniel Baumann --- src/os/dir.go | 125 + src/os/dir_darwin.go | 140 ++ src/os/dir_plan9.go | 86 + src/os/dir_unix.go | 198 ++ src/os/dir_windows.go | 80 + src/os/dirent_aix.go | 30 + src/os/dirent_dragonfly.go | 55 + src/os/dirent_freebsd.go | 47 + src/os/dirent_js.go | 30 + src/os/dirent_linux.go | 51 + src/os/dirent_netbsd.go | 47 + src/os/dirent_openbsd.go | 47 + src/os/dirent_solaris.go | 30 + src/os/dirent_wasip1.go | 52 + src/os/endian_big.go | 9 + src/os/endian_little.go | 9 + src/os/env.go | 141 ++ src/os/env_test.go | 206 ++ src/os/env_unix_test.go | 56 + src/os/error.go | 141 ++ src/os/error_errno.go | 11 + src/os/error_plan9.go | 9 + src/os/error_posix.go | 18 + src/os/error_test.go | 189 ++ src/os/error_unix_test.go | 40 + src/os/error_windows_test.go | 40 + src/os/example_test.go | 265 ++ src/os/exec.go | 180 ++ src/os/exec/bench_test.go | 23 + src/os/exec/dot_test.go | 192 ++ src/os/exec/env_test.go | 67 + src/os/exec/example_test.go | 169 ++ src/os/exec/exec.go | 1303 ++++++++++ src/os/exec/exec_linux_test.go | 45 + src/os/exec/exec_other_test.go | 14 + src/os/exec/exec_plan9.go | 19 + src/os/exec/exec_posix_test.go | 276 +++ src/os/exec/exec_test.go | 1784 ++++++++++++++ src/os/exec/exec_unix.go | 24 + src/os/exec/exec_unix_test.go | 17 + src/os/exec/exec_windows.go | 23 + src/os/exec/exec_windows_test.go | 109 + src/os/exec/internal/fdtest/exists_plan9.go | 20 + src/os/exec/internal/fdtest/exists_test.go | 21 + src/os/exec/internal/fdtest/exists_unix.go | 19 + src/os/exec/internal/fdtest/exists_windows.go | 12 + src/os/exec/internal_test.go | 61 + src/os/exec/lp_linux_test.go | 88 + src/os/exec/lp_plan9.go | 66 + src/os/exec/lp_test.go | 33 + src/os/exec/lp_unix.go | 82 + src/os/exec/lp_unix_test.go | 50 + src/os/exec/lp_wasm.go | 23 + src/os/exec/lp_windows.go | 145 ++ src/os/exec/lp_windows_test.go | 612 +++++ src/os/exec/read3.go | 91 + src/os/exec_plan9.go | 149 ++ src/os/exec_posix.go | 136 + src/os/exec_unix.go | 106 + src/os/exec_unix_test.go | 45 + src/os/exec_windows.go | 181 ++ src/os/executable.go | 20 + src/os/executable_darwin.go | 29 + src/os/executable_dragonfly.go | 12 + src/os/executable_freebsd.go | 12 + src/os/executable_path.go | 104 + src/os/executable_plan9.go | 22 + src/os/executable_procfs.go | 37 + src/os/executable_solaris.go | 32 + src/os/executable_sysctl.go | 35 + src/os/executable_test.go | 155 ++ src/os/executable_wasm.go | 16 + src/os/executable_windows.go | 32 + src/os/export_linux_test.go | 11 + src/os/export_test.go | 17 + src/os/export_unix_test.go | 9 + src/os/export_windows_test.go | 14 + src/os/fifo_test.go | 207 ++ src/os/file.go | 770 ++++++ src/os/file_mutex_plan9.go | 70 + src/os/file_open_unix.go | 17 + src/os/file_open_wasip1.go | 31 + src/os/file_plan9.go | 620 +++++ src/os/file_posix.go | 256 ++ src/os/file_unix.go | 497 ++++ src/os/file_wasip1.go | 22 + src/os/file_windows.go | 524 ++++ src/os/getwd.go | 126 + src/os/os_test.go | 3272 +++++++++++++++++++++++++ src/os/os_unix_test.go | 348 +++ src/os/os_windows_test.go | 1467 +++++++++++ src/os/path.go | 79 + src/os/path_plan9.go | 19 + src/os/path_test.go | 121 + src/os/path_unix.go | 75 + src/os/path_windows.go | 227 ++ src/os/path_windows_test.go | 108 + src/os/pipe2_unix.go | 22 + src/os/pipe_test.go | 478 ++++ src/os/pipe_unix.go | 28 + src/os/pipe_wasm.go | 16 + src/os/proc.go | 80 + src/os/rawconn.go | 47 + src/os/rawconn_test.go | 66 + src/os/read_test.go | 138 ++ src/os/readfrom_linux.go | 124 + src/os/readfrom_linux_test.go | 822 +++++++ src/os/readfrom_stub.go | 13 + src/os/removeall_at.go | 199 ++ src/os/removeall_noat.go | 142 ++ src/os/removeall_test.go | 506 ++++ src/os/signal/doc.go | 232 ++ src/os/signal/example_test.go | 38 + src/os/signal/example_unix_test.go | 47 + src/os/signal/sig.s | 8 + src/os/signal/signal.go | 334 +++ src/os/signal/signal_cgo_test.go | 350 +++ src/os/signal/signal_linux_test.go | 42 + src/os/signal/signal_plan9.go | 64 + src/os/signal/signal_plan9_test.go | 167 ++ src/os/signal/signal_test.go | 932 +++++++ src/os/signal/signal_unix.go | 62 + src/os/signal/signal_windows_test.go | 98 + src/os/stat.go | 23 + src/os/stat_aix.go | 51 + src/os/stat_darwin.go | 47 + src/os/stat_dragonfly.go | 47 + src/os/stat_freebsd.go | 47 + src/os/stat_js.go | 50 + src/os/stat_linux.go | 47 + src/os/stat_netbsd.go | 47 + src/os/stat_openbsd.go | 47 + src/os/stat_plan9.go | 114 + src/os/stat_solaris.go | 57 + src/os/stat_test.go | 296 +++ src/os/stat_unix.go | 52 + src/os/stat_wasip1.go | 40 + src/os/stat_windows.go | 136 + src/os/sticky_bsd.go | 11 + src/os/sticky_notbsd.go | 9 + src/os/str.go | 39 + src/os/sys.go | 10 + src/os/sys_aix.go | 26 + src/os/sys_bsd.go | 17 + src/os/sys_js.go | 11 + src/os/sys_linux.go | 53 + src/os/sys_plan9.go | 24 + src/os/sys_solaris.go | 11 + src/os/sys_unix.go | 14 + src/os/sys_wasip1.go | 11 + src/os/sys_windows.go | 33 + src/os/tempfile.go | 128 + src/os/tempfile_test.go | 205 ++ src/os/testdata/dirfs/a | 0 src/os/testdata/dirfs/b | 0 src/os/testdata/dirfs/dir/x | 0 src/os/testdata/hello | 1 + src/os/testdata/issue37161/a | 1 + src/os/testdata/issue37161/b | 1 + src/os/testdata/issue37161/c | 1 + src/os/timeout_test.go | 708 ++++++ src/os/types.go | 74 + src/os/types_plan9.go | 30 + src/os/types_unix.go | 30 + src/os/types_windows.go | 262 ++ src/os/user/cgo_listgroups_unix.go | 57 + src/os/user/cgo_lookup_cgo.go | 112 + src/os/user/cgo_lookup_syscall.go | 65 + src/os/user/cgo_lookup_unix.go | 200 ++ src/os/user/cgo_unix_test.go | 23 + src/os/user/cgo_user_test.go | 11 + src/os/user/getgrouplist_syscall.go | 19 + src/os/user/getgrouplist_unix.go | 22 + src/os/user/listgroups_stub.go | 19 + src/os/user/listgroups_unix.go | 109 + src/os/user/listgroups_unix_test.go | 107 + src/os/user/lookup.go | 70 + src/os/user/lookup_android.go | 25 + src/os/user/lookup_plan9.go | 67 + src/os/user/lookup_stubs.go | 83 + src/os/user/lookup_unix.go | 234 ++ src/os/user/lookup_unix_test.go | 259 ++ src/os/user/lookup_windows.go | 392 +++ src/os/user/user.go | 95 + src/os/user/user_test.go | 192 ++ src/os/wait6_dragonfly.go | 18 + src/os/wait6_freebsd64.go | 20 + src/os/wait6_freebsd_386.go | 18 + src/os/wait6_freebsd_arm.go | 18 + src/os/wait6_netbsd.go | 18 + src/os/wait_unimp.go | 21 + src/os/wait_wait6.go | 32 + src/os/wait_waitid.go | 48 + 193 files changed, 27970 insertions(+) create mode 100644 src/os/dir.go create mode 100644 src/os/dir_darwin.go create mode 100644 src/os/dir_plan9.go create mode 100644 src/os/dir_unix.go create mode 100644 src/os/dir_windows.go create mode 100644 src/os/dirent_aix.go create mode 100644 src/os/dirent_dragonfly.go create mode 100644 src/os/dirent_freebsd.go create mode 100644 src/os/dirent_js.go create mode 100644 src/os/dirent_linux.go create mode 100644 src/os/dirent_netbsd.go create mode 100644 src/os/dirent_openbsd.go create mode 100644 src/os/dirent_solaris.go create mode 100644 src/os/dirent_wasip1.go create mode 100644 src/os/endian_big.go create mode 100644 src/os/endian_little.go create mode 100644 src/os/env.go create mode 100644 src/os/env_test.go create mode 100644 src/os/env_unix_test.go create mode 100644 src/os/error.go create mode 100644 src/os/error_errno.go create mode 100644 src/os/error_plan9.go create mode 100644 src/os/error_posix.go create mode 100644 src/os/error_test.go create mode 100644 src/os/error_unix_test.go create mode 100644 src/os/error_windows_test.go create mode 100644 src/os/example_test.go create mode 100644 src/os/exec.go create mode 100644 src/os/exec/bench_test.go create mode 100644 src/os/exec/dot_test.go create mode 100644 src/os/exec/env_test.go create mode 100644 src/os/exec/example_test.go create mode 100644 src/os/exec/exec.go create mode 100644 src/os/exec/exec_linux_test.go create mode 100644 src/os/exec/exec_other_test.go create mode 100644 src/os/exec/exec_plan9.go create mode 100644 src/os/exec/exec_posix_test.go create mode 100644 src/os/exec/exec_test.go create mode 100644 src/os/exec/exec_unix.go create mode 100644 src/os/exec/exec_unix_test.go create mode 100644 src/os/exec/exec_windows.go create mode 100644 src/os/exec/exec_windows_test.go create mode 100644 src/os/exec/internal/fdtest/exists_plan9.go create mode 100644 src/os/exec/internal/fdtest/exists_test.go create mode 100644 src/os/exec/internal/fdtest/exists_unix.go create mode 100644 src/os/exec/internal/fdtest/exists_windows.go create mode 100644 src/os/exec/internal_test.go create mode 100644 src/os/exec/lp_linux_test.go create mode 100644 src/os/exec/lp_plan9.go create mode 100644 src/os/exec/lp_test.go create mode 100644 src/os/exec/lp_unix.go create mode 100644 src/os/exec/lp_unix_test.go create mode 100644 src/os/exec/lp_wasm.go create mode 100644 src/os/exec/lp_windows.go create mode 100644 src/os/exec/lp_windows_test.go create mode 100644 src/os/exec/read3.go create mode 100644 src/os/exec_plan9.go create mode 100644 src/os/exec_posix.go create mode 100644 src/os/exec_unix.go create mode 100644 src/os/exec_unix_test.go create mode 100644 src/os/exec_windows.go create mode 100644 src/os/executable.go create mode 100644 src/os/executable_darwin.go create mode 100644 src/os/executable_dragonfly.go create mode 100644 src/os/executable_freebsd.go create mode 100644 src/os/executable_path.go create mode 100644 src/os/executable_plan9.go create mode 100644 src/os/executable_procfs.go create mode 100644 src/os/executable_solaris.go create mode 100644 src/os/executable_sysctl.go create mode 100644 src/os/executable_test.go create mode 100644 src/os/executable_wasm.go create mode 100644 src/os/executable_windows.go create mode 100644 src/os/export_linux_test.go create mode 100644 src/os/export_test.go create mode 100644 src/os/export_unix_test.go create mode 100644 src/os/export_windows_test.go create mode 100644 src/os/fifo_test.go create mode 100644 src/os/file.go create mode 100644 src/os/file_mutex_plan9.go create mode 100644 src/os/file_open_unix.go create mode 100644 src/os/file_open_wasip1.go create mode 100644 src/os/file_plan9.go create mode 100644 src/os/file_posix.go create mode 100644 src/os/file_unix.go create mode 100644 src/os/file_wasip1.go create mode 100644 src/os/file_windows.go create mode 100644 src/os/getwd.go create mode 100644 src/os/os_test.go create mode 100644 src/os/os_unix_test.go create mode 100644 src/os/os_windows_test.go create mode 100644 src/os/path.go create mode 100644 src/os/path_plan9.go create mode 100644 src/os/path_test.go create mode 100644 src/os/path_unix.go create mode 100644 src/os/path_windows.go create mode 100644 src/os/path_windows_test.go create mode 100644 src/os/pipe2_unix.go create mode 100644 src/os/pipe_test.go create mode 100644 src/os/pipe_unix.go create mode 100644 src/os/pipe_wasm.go create mode 100644 src/os/proc.go create mode 100644 src/os/rawconn.go create mode 100644 src/os/rawconn_test.go create mode 100644 src/os/read_test.go create mode 100644 src/os/readfrom_linux.go create mode 100644 src/os/readfrom_linux_test.go create mode 100644 src/os/readfrom_stub.go create mode 100644 src/os/removeall_at.go create mode 100644 src/os/removeall_noat.go create mode 100644 src/os/removeall_test.go create mode 100644 src/os/signal/doc.go create mode 100644 src/os/signal/example_test.go create mode 100644 src/os/signal/example_unix_test.go create mode 100644 src/os/signal/sig.s create mode 100644 src/os/signal/signal.go create mode 100644 src/os/signal/signal_cgo_test.go create mode 100644 src/os/signal/signal_linux_test.go create mode 100644 src/os/signal/signal_plan9.go create mode 100644 src/os/signal/signal_plan9_test.go create mode 100644 src/os/signal/signal_test.go create mode 100644 src/os/signal/signal_unix.go create mode 100644 src/os/signal/signal_windows_test.go create mode 100644 src/os/stat.go create mode 100644 src/os/stat_aix.go create mode 100644 src/os/stat_darwin.go create mode 100644 src/os/stat_dragonfly.go create mode 100644 src/os/stat_freebsd.go create mode 100644 src/os/stat_js.go create mode 100644 src/os/stat_linux.go create mode 100644 src/os/stat_netbsd.go create mode 100644 src/os/stat_openbsd.go create mode 100644 src/os/stat_plan9.go create mode 100644 src/os/stat_solaris.go create mode 100644 src/os/stat_test.go create mode 100644 src/os/stat_unix.go create mode 100644 src/os/stat_wasip1.go create mode 100644 src/os/stat_windows.go create mode 100644 src/os/sticky_bsd.go create mode 100644 src/os/sticky_notbsd.go create mode 100644 src/os/str.go create mode 100644 src/os/sys.go create mode 100644 src/os/sys_aix.go create mode 100644 src/os/sys_bsd.go create mode 100644 src/os/sys_js.go create mode 100644 src/os/sys_linux.go create mode 100644 src/os/sys_plan9.go create mode 100644 src/os/sys_solaris.go create mode 100644 src/os/sys_unix.go create mode 100644 src/os/sys_wasip1.go create mode 100644 src/os/sys_windows.go create mode 100644 src/os/tempfile.go create mode 100644 src/os/tempfile_test.go create mode 100644 src/os/testdata/dirfs/a create mode 100644 src/os/testdata/dirfs/b create mode 100644 src/os/testdata/dirfs/dir/x create mode 100644 src/os/testdata/hello create mode 100644 src/os/testdata/issue37161/a create mode 100644 src/os/testdata/issue37161/b create mode 100644 src/os/testdata/issue37161/c create mode 100644 src/os/timeout_test.go create mode 100644 src/os/types.go create mode 100644 src/os/types_plan9.go create mode 100644 src/os/types_unix.go create mode 100644 src/os/types_windows.go create mode 100644 src/os/user/cgo_listgroups_unix.go create mode 100644 src/os/user/cgo_lookup_cgo.go create mode 100644 src/os/user/cgo_lookup_syscall.go create mode 100644 src/os/user/cgo_lookup_unix.go create mode 100644 src/os/user/cgo_unix_test.go create mode 100644 src/os/user/cgo_user_test.go create mode 100644 src/os/user/getgrouplist_syscall.go create mode 100644 src/os/user/getgrouplist_unix.go create mode 100644 src/os/user/listgroups_stub.go create mode 100644 src/os/user/listgroups_unix.go create mode 100644 src/os/user/listgroups_unix_test.go create mode 100644 src/os/user/lookup.go create mode 100644 src/os/user/lookup_android.go create mode 100644 src/os/user/lookup_plan9.go create mode 100644 src/os/user/lookup_stubs.go create mode 100644 src/os/user/lookup_unix.go create mode 100644 src/os/user/lookup_unix_test.go create mode 100644 src/os/user/lookup_windows.go create mode 100644 src/os/user/user.go create mode 100644 src/os/user/user_test.go create mode 100644 src/os/wait6_dragonfly.go create mode 100644 src/os/wait6_freebsd64.go create mode 100644 src/os/wait6_freebsd_386.go create mode 100644 src/os/wait6_freebsd_arm.go create mode 100644 src/os/wait6_netbsd.go create mode 100644 src/os/wait_unimp.go create mode 100644 src/os/wait_wait6.go create mode 100644 src/os/wait_waitid.go (limited to 'src/os') diff --git a/src/os/dir.go b/src/os/dir.go new file mode 100644 index 0000000..5306bcb --- /dev/null +++ b/src/os/dir.go @@ -0,0 +1,125 @@ +// Copyright 2016 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 os + +import ( + "io/fs" + "sort" +) + +type readdirMode int + +const ( + readdirName readdirMode = iota + readdirDirEntry + readdirFileInfo +) + +// Readdir reads the contents of the directory associated with file and +// returns a slice of up to n FileInfo values, as would be returned +// by Lstat, in directory order. Subsequent calls on the same file will yield +// further FileInfos. +// +// If n > 0, Readdir returns at most n FileInfo 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. +// +// If n <= 0, Readdir returns all the FileInfo 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 FileInfo read until that point +// and a non-nil error. +// +// Most clients are better served by the more efficient ReadDir method. +func (f *File) Readdir(n int) ([]FileInfo, error) { + if f == nil { + return nil, ErrInvalid + } + _, _, infos, err := f.readdir(n, readdirFileInfo) + if infos == nil { + // Readdir has historically always returned a non-nil empty slice, never nil, + // even on error (except misuse with nil receiver above). + // Keep it that way to avoid breaking overly sensitive callers. + infos = []FileInfo{} + } + return infos, err +} + +// Readdirnames reads the contents of the directory associated with file +// and returns a slice of up to n names of files in the directory, +// in directory order. Subsequent calls on the same file will yield +// further names. +// +// If n > 0, Readdirnames returns at most n names. In this case, if +// Readdirnames returns an empty slice, it will return a non-nil error +// explaining why. At the end of a directory, the error is io.EOF. +// +// If n <= 0, Readdirnames returns all the names from the directory in +// a single slice. In this case, if Readdirnames 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, Readdirnames returns the names read until that point and +// a non-nil error. +func (f *File) Readdirnames(n int) (names []string, err error) { + if f == nil { + return nil, ErrInvalid + } + names, _, _, err = f.readdir(n, readdirName) + if names == nil { + // Readdirnames has historically always returned a non-nil empty slice, never nil, + // even on error (except misuse with nil receiver above). + // Keep it that way to avoid breaking overly sensitive callers. + names = []string{} + } + return names, err +} + +// A DirEntry is an entry read from a directory +// (using the ReadDir function or a File's ReadDir method). +type DirEntry = fs.DirEntry + +// ReadDir reads the contents of the directory associated with the file f +// and returns a slice of DirEntry values in directory order. +// Subsequent calls on the same file will yield later DirEntry records in the directory. +// +// If n > 0, ReadDir returns at most n DirEntry records. +// In this case, if ReadDir returns an empty slice, it will return an error explaining why. +// At the end of a directory, the error is io.EOF. +// +// If n <= 0, ReadDir returns all the DirEntry records remaining in the directory. +// When it succeeds, it returns a nil error (not io.EOF). +func (f *File) ReadDir(n int) ([]DirEntry, error) { + if f == nil { + return nil, ErrInvalid + } + _, dirents, _, err := f.readdir(n, readdirDirEntry) + if dirents == nil { + // Match Readdir and Readdirnames: don't return nil slices. + dirents = []DirEntry{} + } + return dirents, err +} + +// testingForceReadDirLstat forces ReadDir to call Lstat, for testing that code path. +// This can be difficult to provoke on some Unix systems otherwise. +var testingForceReadDirLstat bool + +// ReadDir reads the named directory, +// returning all its directory entries sorted by filename. +// If an error occurs reading the directory, +// ReadDir returns the entries it was able to read before the error, +// along with the error. +func ReadDir(name string) ([]DirEntry, error) { + f, err := Open(name) + if err != nil { + return nil, err + } + defer f.Close() + + dirs, err := f.ReadDir(-1) + sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) + return dirs, err +} diff --git a/src/os/dir_darwin.go b/src/os/dir_darwin.go new file mode 100644 index 0000000..e6d5bda --- /dev/null +++ b/src/os/dir_darwin.go @@ -0,0 +1,140 @@ +// Copyright 2009 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 os + +import ( + "io" + "runtime" + "syscall" + "unsafe" +) + +// Auxiliary information if the File describes a directory +type dirInfo struct { + dir uintptr // Pointer to DIR structure from dirent.h +} + +func (d *dirInfo) close() { + if d.dir == 0 { + return + } + closedir(d.dir) + d.dir = 0 +} + +func (f *File) readdir(n int, mode readdirMode) (names []string, dirents []DirEntry, infos []FileInfo, err error) { + if f.dirinfo == nil { + dir, call, errno := f.pfd.OpenDir() + if errno != nil { + return nil, nil, nil, &PathError{Op: call, Path: f.name, Err: errno} + } + f.dirinfo = &dirInfo{ + dir: dir, + } + } + d := f.dirinfo + + size := n + if size <= 0 { + size = 100 + n = -1 + } + + var dirent syscall.Dirent + var entptr *syscall.Dirent + for len(names)+len(dirents)+len(infos) < size || n == -1 { + if errno := readdir_r(d.dir, &dirent, &entptr); errno != 0 { + if errno == syscall.EINTR { + continue + } + return names, dirents, infos, &PathError{Op: "readdir", Path: f.name, Err: errno} + } + if entptr == nil { // EOF + break + } + // Darwin may return a zero inode when a directory entry has been + // deleted but not yet removed from the directory. The man page for + // getdirentries(2) states that programs are responsible for skipping + // those entries: + // + // Users of getdirentries() should skip entries with d_fileno = 0, + // as such entries represent files which have been deleted but not + // yet removed from the directory entry. + // + if dirent.Ino == 0 { + continue + } + name := (*[len(syscall.Dirent{}.Name)]byte)(unsafe.Pointer(&dirent.Name))[:] + for i, c := range name { + if c == 0 { + name = name[:i] + break + } + } + // Check for useless names before allocating a string. + if string(name) == "." || string(name) == ".." { + continue + } + if mode == readdirName { + names = append(names, string(name)) + } else if mode == readdirDirEntry { + de, err := newUnixDirent(f.name, string(name), dtToType(dirent.Type)) + if IsNotExist(err) { + // File disappeared between readdir and stat. + // Treat as if it didn't exist. + continue + } + if err != nil { + return nil, dirents, nil, err + } + dirents = append(dirents, de) + } else { + info, err := lstat(f.name + "/" + string(name)) + if IsNotExist(err) { + // File disappeared between readdir + stat. + // Treat as if it didn't exist. + continue + } + if err != nil { + return nil, nil, infos, err + } + infos = append(infos, info) + } + runtime.KeepAlive(f) + } + + if n > 0 && len(names)+len(dirents)+len(infos) == 0 { + return nil, nil, nil, io.EOF + } + return names, dirents, infos, nil +} + +func dtToType(typ uint8) FileMode { + switch typ { + case syscall.DT_BLK: + return ModeDevice + case syscall.DT_CHR: + return ModeDevice | ModeCharDevice + case syscall.DT_DIR: + return ModeDir + case syscall.DT_FIFO: + return ModeNamedPipe + case syscall.DT_LNK: + return ModeSymlink + case syscall.DT_REG: + return 0 + case syscall.DT_SOCK: + return ModeSocket + } + return ^FileMode(0) +} + +// Implemented in syscall/syscall_darwin.go. + +//go:linkname closedir syscall.closedir +func closedir(dir uintptr) (err error) + +//go:linkname readdir_r syscall.readdir_r +func readdir_r(dir uintptr, entry *syscall.Dirent, result **syscall.Dirent) (res syscall.Errno) diff --git a/src/os/dir_plan9.go b/src/os/dir_plan9.go new file mode 100644 index 0000000..6ea5940 --- /dev/null +++ b/src/os/dir_plan9.go @@ -0,0 +1,86 @@ +// Copyright 2009 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 os + +import ( + "io" + "io/fs" + "syscall" +) + +func (file *File) readdir(n int, mode readdirMode) (names []string, dirents []DirEntry, infos []FileInfo, err error) { + // If this file has no dirinfo, create one. + if file.dirinfo == nil { + file.dirinfo = new(dirInfo) + } + d := file.dirinfo + size := n + if size <= 0 { + size = 100 + n = -1 + } + for n != 0 { + // Refill the buffer if necessary. + if d.bufp >= d.nbuf { + nb, err := file.Read(d.buf[:]) + + // Update the buffer state before checking for errors. + d.bufp, d.nbuf = 0, nb + + if err != nil { + if err == io.EOF { + break + } + return names, dirents, infos, &PathError{Op: "readdir", Path: file.name, Err: err} + } + if nb < syscall.STATFIXLEN { + return names, dirents, infos, &PathError{Op: "readdir", Path: file.name, Err: syscall.ErrShortStat} + } + } + + // Get a record from the buffer. + b := d.buf[d.bufp:] + m := int(uint16(b[0])|uint16(b[1])<<8) + 2 + if m < syscall.STATFIXLEN { + return names, dirents, infos, &PathError{Op: "readdir", Path: file.name, Err: syscall.ErrShortStat} + } + + dir, err := syscall.UnmarshalDir(b[:m]) + if err != nil { + return names, dirents, infos, &PathError{Op: "readdir", Path: file.name, Err: err} + } + + if mode == readdirName { + names = append(names, dir.Name) + } else { + f := fileInfoFromStat(dir) + if mode == readdirDirEntry { + dirents = append(dirents, dirEntry{f}) + } else { + infos = append(infos, f) + } + } + d.bufp += m + n-- + } + + if n > 0 && len(names)+len(dirents)+len(infos) == 0 { + return nil, nil, nil, io.EOF + } + return names, dirents, infos, nil +} + +type dirEntry struct { + fs *fileStat +} + +func (de dirEntry) Name() string { return de.fs.Name() } +func (de dirEntry) IsDir() bool { return de.fs.IsDir() } +func (de dirEntry) Type() FileMode { return de.fs.Mode().Type() } +func (de dirEntry) Info() (FileInfo, error) { return de.fs, nil } + +func (de dirEntry) String() string { + return fs.FormatDirEntry(de) +} diff --git a/src/os/dir_unix.go b/src/os/dir_unix.go new file mode 100644 index 0000000..266a78a --- /dev/null +++ b/src/os/dir_unix.go @@ -0,0 +1,198 @@ +// Copyright 2009 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. + +//go:build aix || dragonfly || freebsd || (js && wasm) || wasip1 || linux || netbsd || openbsd || solaris + +package os + +import ( + "io" + "runtime" + "sync" + "syscall" + "unsafe" +) + +// Auxiliary information if the File describes a directory +type dirInfo struct { + buf *[]byte // buffer for directory I/O + nbuf int // length of buf; return value from Getdirentries + bufp int // location of next record in buf. +} + +const ( + // More than 5760 to work around https://golang.org/issue/24015. + blockSize = 8192 +) + +var dirBufPool = sync.Pool{ + New: func() any { + // The buffer must be at least a block long. + buf := make([]byte, blockSize) + return &buf + }, +} + +func (d *dirInfo) close() { + if d.buf != nil { + dirBufPool.Put(d.buf) + d.buf = nil + } +} + +func (f *File) readdir(n int, mode readdirMode) (names []string, dirents []DirEntry, infos []FileInfo, err error) { + // If this file has no dirinfo, create one. + if f.dirinfo == nil { + f.dirinfo = new(dirInfo) + f.dirinfo.buf = dirBufPool.Get().(*[]byte) + } + d := f.dirinfo + + // Change the meaning of n for the implementation below. + // + // The n above was for the public interface of "if n <= 0, + // Readdir returns all the FileInfo from the directory in a + // single slice". + // + // But below, we use only negative to mean looping until the + // end and positive to mean bounded, with positive + // terminating at 0. + if n == 0 { + n = -1 + } + + for n != 0 { + // Refill the buffer if necessary + if d.bufp >= d.nbuf { + d.bufp = 0 + var errno error + d.nbuf, errno = f.pfd.ReadDirent(*d.buf) + runtime.KeepAlive(f) + if errno != nil { + return names, dirents, infos, &PathError{Op: "readdirent", Path: f.name, Err: errno} + } + if d.nbuf <= 0 { + break // EOF + } + } + + // Drain the buffer + buf := (*d.buf)[d.bufp:d.nbuf] + reclen, ok := direntReclen(buf) + if !ok || reclen > uint64(len(buf)) { + break + } + rec := buf[:reclen] + d.bufp += int(reclen) + ino, ok := direntIno(rec) + if !ok { + break + } + // When building to wasip1, the host runtime might be running on Windows + // or might expose a remote file system which does not have the concept + // of inodes. Therefore, we cannot make the assumption that it is safe + // to skip entries with zero inodes. + if ino == 0 && runtime.GOOS != "wasip1" { + continue + } + const namoff = uint64(unsafe.Offsetof(syscall.Dirent{}.Name)) + namlen, ok := direntNamlen(rec) + if !ok || namoff+namlen > uint64(len(rec)) { + break + } + name := rec[namoff : namoff+namlen] + for i, c := range name { + if c == 0 { + name = name[:i] + break + } + } + // Check for useless names before allocating a string. + if string(name) == "." || string(name) == ".." { + continue + } + if n > 0 { // see 'n == 0' comment above + n-- + } + if mode == readdirName { + names = append(names, string(name)) + } else if mode == readdirDirEntry { + de, err := newUnixDirent(f.name, string(name), direntType(rec)) + if IsNotExist(err) { + // File disappeared between readdir and stat. + // Treat as if it didn't exist. + continue + } + if err != nil { + return nil, dirents, nil, err + } + dirents = append(dirents, de) + } else { + info, err := lstat(f.name + "/" + string(name)) + if IsNotExist(err) { + // File disappeared between readdir + stat. + // Treat as if it didn't exist. + continue + } + if err != nil { + return nil, nil, infos, err + } + infos = append(infos, info) + } + } + + if n > 0 && len(names)+len(dirents)+len(infos) == 0 { + return nil, nil, nil, io.EOF + } + return names, dirents, infos, nil +} + +// readInt returns the size-bytes unsigned integer in native byte order at offset off. +func readInt(b []byte, off, size uintptr) (u uint64, ok bool) { + if len(b) < int(off+size) { + return 0, false + } + if isBigEndian { + return readIntBE(b[off:], size), true + } + return readIntLE(b[off:], size), true +} + +func readIntBE(b []byte, size uintptr) uint64 { + switch size { + case 1: + return uint64(b[0]) + case 2: + _ = b[1] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[1]) | uint64(b[0])<<8 + case 4: + _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[3]) | uint64(b[2])<<8 | uint64(b[1])<<16 | uint64(b[0])<<24 + case 8: + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 + default: + panic("syscall: readInt with unsupported size") + } +} + +func readIntLE(b []byte, size uintptr) uint64 { + switch size { + case 1: + return uint64(b[0]) + case 2: + _ = b[1] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[0]) | uint64(b[1])<<8 + case 4: + _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 + case 8: + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | + uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 + default: + panic("syscall: readInt with unsupported size") + } +} diff --git a/src/os/dir_windows.go b/src/os/dir_windows.go new file mode 100644 index 0000000..9dc2cd7 --- /dev/null +++ b/src/os/dir_windows.go @@ -0,0 +1,80 @@ +// Copyright 2009 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 os + +import ( + "io" + "io/fs" + "runtime" + "syscall" +) + +func (file *File) readdir(n int, mode readdirMode) (names []string, dirents []DirEntry, infos []FileInfo, err error) { + // If this file has no dirinfo, create one. + needdata := true + if file.dirinfo == nil { + needdata = false + file.dirinfo, err = openDir(file.name) + if err != nil { + err = &PathError{Op: "readdir", Path: file.name, Err: err} + return + } + } + wantAll := n <= 0 + if wantAll { + n = -1 + } + d := &file.dirinfo.data + for n != 0 && !file.dirinfo.isempty { + if needdata { + e := syscall.FindNextFile(file.dirinfo.h, d) + runtime.KeepAlive(file) + if e != nil { + if e == syscall.ERROR_NO_MORE_FILES { + break + } else { + err = &PathError{Op: "FindNextFile", Path: file.name, Err: e} + return + } + } + } + needdata = true + name := syscall.UTF16ToString(d.FileName[0:]) + if name == "." || name == ".." { // Useless names + continue + } + if mode == readdirName { + names = append(names, name) + } else { + f := newFileStatFromWin32finddata(d) + f.name = name + f.path = file.dirinfo.path + f.appendNameToPath = true + if mode == readdirDirEntry { + dirents = append(dirents, dirEntry{f}) + } else { + infos = append(infos, f) + } + } + n-- + } + if !wantAll && len(names)+len(dirents)+len(infos) == 0 { + return nil, nil, nil, io.EOF + } + return names, dirents, infos, nil +} + +type dirEntry struct { + fs *fileStat +} + +func (de dirEntry) Name() string { return de.fs.Name() } +func (de dirEntry) IsDir() bool { return de.fs.IsDir() } +func (de dirEntry) Type() FileMode { return de.fs.Mode().Type() } +func (de dirEntry) Info() (FileInfo, error) { return de.fs, nil } + +func (de dirEntry) String() string { + return fs.FormatDirEntry(de) +} diff --git a/src/os/dirent_aix.go b/src/os/dirent_aix.go new file mode 100644 index 0000000..5597b8a --- /dev/null +++ b/src/os/dirent_aix.go @@ -0,0 +1,30 @@ +// 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 os + +import ( + "syscall" + "unsafe" +) + +func direntIno(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Ino), unsafe.Sizeof(syscall.Dirent{}.Ino)) +} + +func direntReclen(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Reclen), unsafe.Sizeof(syscall.Dirent{}.Reclen)) +} + +func direntNamlen(buf []byte) (uint64, bool) { + reclen, ok := direntReclen(buf) + if !ok { + return 0, false + } + return reclen - uint64(unsafe.Offsetof(syscall.Dirent{}.Name)), true +} + +func direntType(buf []byte) FileMode { + return ^FileMode(0) // unknown +} diff --git a/src/os/dirent_dragonfly.go b/src/os/dirent_dragonfly.go new file mode 100644 index 0000000..38cbd61 --- /dev/null +++ b/src/os/dirent_dragonfly.go @@ -0,0 +1,55 @@ +// 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 os + +import ( + "syscall" + "unsafe" +) + +func direntIno(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Fileno), unsafe.Sizeof(syscall.Dirent{}.Fileno)) +} + +func direntReclen(buf []byte) (uint64, bool) { + namlen, ok := direntNamlen(buf) + if !ok { + return 0, false + } + return (16 + namlen + 1 + 7) &^ 7, true +} + +func direntNamlen(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Namlen), unsafe.Sizeof(syscall.Dirent{}.Namlen)) +} + +func direntType(buf []byte) FileMode { + off := unsafe.Offsetof(syscall.Dirent{}.Type) + if off >= uintptr(len(buf)) { + return ^FileMode(0) // unknown + } + typ := buf[off] + switch typ { + case syscall.DT_BLK: + return ModeDevice + case syscall.DT_CHR: + return ModeDevice | ModeCharDevice + case syscall.DT_DBF: + // DT_DBF is "database record file". + // fillFileStatFromSys treats as regular file. + return 0 + case syscall.DT_DIR: + return ModeDir + case syscall.DT_FIFO: + return ModeNamedPipe + case syscall.DT_LNK: + return ModeSymlink + case syscall.DT_REG: + return 0 + case syscall.DT_SOCK: + return ModeSocket + } + return ^FileMode(0) // unknown +} diff --git a/src/os/dirent_freebsd.go b/src/os/dirent_freebsd.go new file mode 100644 index 0000000..d600837 --- /dev/null +++ b/src/os/dirent_freebsd.go @@ -0,0 +1,47 @@ +// 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 os + +import ( + "syscall" + "unsafe" +) + +func direntIno(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Fileno), unsafe.Sizeof(syscall.Dirent{}.Fileno)) +} + +func direntReclen(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Reclen), unsafe.Sizeof(syscall.Dirent{}.Reclen)) +} + +func direntNamlen(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Namlen), unsafe.Sizeof(syscall.Dirent{}.Namlen)) +} + +func direntType(buf []byte) FileMode { + off := unsafe.Offsetof(syscall.Dirent{}.Type) + if off >= uintptr(len(buf)) { + return ^FileMode(0) // unknown + } + typ := buf[off] + switch typ { + case syscall.DT_BLK: + return ModeDevice + case syscall.DT_CHR: + return ModeDevice | ModeCharDevice + case syscall.DT_DIR: + return ModeDir + case syscall.DT_FIFO: + return ModeNamedPipe + case syscall.DT_LNK: + return ModeSymlink + case syscall.DT_REG: + return 0 + case syscall.DT_SOCK: + return ModeSocket + } + return ^FileMode(0) // unknown +} diff --git a/src/os/dirent_js.go b/src/os/dirent_js.go new file mode 100644 index 0000000..31778c2 --- /dev/null +++ b/src/os/dirent_js.go @@ -0,0 +1,30 @@ +// 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 os + +import ( + "syscall" + "unsafe" +) + +func direntIno(buf []byte) (uint64, bool) { + return 1, true +} + +func direntReclen(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Reclen), unsafe.Sizeof(syscall.Dirent{}.Reclen)) +} + +func direntNamlen(buf []byte) (uint64, bool) { + reclen, ok := direntReclen(buf) + if !ok { + return 0, false + } + return reclen - uint64(unsafe.Offsetof(syscall.Dirent{}.Name)), true +} + +func direntType(buf []byte) FileMode { + return ^FileMode(0) // unknown +} diff --git a/src/os/dirent_linux.go b/src/os/dirent_linux.go new file mode 100644 index 0000000..74a3431 --- /dev/null +++ b/src/os/dirent_linux.go @@ -0,0 +1,51 @@ +// 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 os + +import ( + "syscall" + "unsafe" +) + +func direntIno(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Ino), unsafe.Sizeof(syscall.Dirent{}.Ino)) +} + +func direntReclen(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Reclen), unsafe.Sizeof(syscall.Dirent{}.Reclen)) +} + +func direntNamlen(buf []byte) (uint64, bool) { + reclen, ok := direntReclen(buf) + if !ok { + return 0, false + } + return reclen - uint64(unsafe.Offsetof(syscall.Dirent{}.Name)), true +} + +func direntType(buf []byte) FileMode { + off := unsafe.Offsetof(syscall.Dirent{}.Type) + if off >= uintptr(len(buf)) { + return ^FileMode(0) // unknown + } + typ := buf[off] + switch typ { + case syscall.DT_BLK: + return ModeDevice + case syscall.DT_CHR: + return ModeDevice | ModeCharDevice + case syscall.DT_DIR: + return ModeDir + case syscall.DT_FIFO: + return ModeNamedPipe + case syscall.DT_LNK: + return ModeSymlink + case syscall.DT_REG: + return 0 + case syscall.DT_SOCK: + return ModeSocket + } + return ^FileMode(0) // unknown +} diff --git a/src/os/dirent_netbsd.go b/src/os/dirent_netbsd.go new file mode 100644 index 0000000..d600837 --- /dev/null +++ b/src/os/dirent_netbsd.go @@ -0,0 +1,47 @@ +// 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 os + +import ( + "syscall" + "unsafe" +) + +func direntIno(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Fileno), unsafe.Sizeof(syscall.Dirent{}.Fileno)) +} + +func direntReclen(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Reclen), unsafe.Sizeof(syscall.Dirent{}.Reclen)) +} + +func direntNamlen(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Namlen), unsafe.Sizeof(syscall.Dirent{}.Namlen)) +} + +func direntType(buf []byte) FileMode { + off := unsafe.Offsetof(syscall.Dirent{}.Type) + if off >= uintptr(len(buf)) { + return ^FileMode(0) // unknown + } + typ := buf[off] + switch typ { + case syscall.DT_BLK: + return ModeDevice + case syscall.DT_CHR: + return ModeDevice | ModeCharDevice + case syscall.DT_DIR: + return ModeDir + case syscall.DT_FIFO: + return ModeNamedPipe + case syscall.DT_LNK: + return ModeSymlink + case syscall.DT_REG: + return 0 + case syscall.DT_SOCK: + return ModeSocket + } + return ^FileMode(0) // unknown +} diff --git a/src/os/dirent_openbsd.go b/src/os/dirent_openbsd.go new file mode 100644 index 0000000..d600837 --- /dev/null +++ b/src/os/dirent_openbsd.go @@ -0,0 +1,47 @@ +// 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 os + +import ( + "syscall" + "unsafe" +) + +func direntIno(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Fileno), unsafe.Sizeof(syscall.Dirent{}.Fileno)) +} + +func direntReclen(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Reclen), unsafe.Sizeof(syscall.Dirent{}.Reclen)) +} + +func direntNamlen(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Namlen), unsafe.Sizeof(syscall.Dirent{}.Namlen)) +} + +func direntType(buf []byte) FileMode { + off := unsafe.Offsetof(syscall.Dirent{}.Type) + if off >= uintptr(len(buf)) { + return ^FileMode(0) // unknown + } + typ := buf[off] + switch typ { + case syscall.DT_BLK: + return ModeDevice + case syscall.DT_CHR: + return ModeDevice | ModeCharDevice + case syscall.DT_DIR: + return ModeDir + case syscall.DT_FIFO: + return ModeNamedPipe + case syscall.DT_LNK: + return ModeSymlink + case syscall.DT_REG: + return 0 + case syscall.DT_SOCK: + return ModeSocket + } + return ^FileMode(0) // unknown +} diff --git a/src/os/dirent_solaris.go b/src/os/dirent_solaris.go new file mode 100644 index 0000000..5597b8a --- /dev/null +++ b/src/os/dirent_solaris.go @@ -0,0 +1,30 @@ +// 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 os + +import ( + "syscall" + "unsafe" +) + +func direntIno(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Ino), unsafe.Sizeof(syscall.Dirent{}.Ino)) +} + +func direntReclen(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Reclen), unsafe.Sizeof(syscall.Dirent{}.Reclen)) +} + +func direntNamlen(buf []byte) (uint64, bool) { + reclen, ok := direntReclen(buf) + if !ok { + return 0, false + } + return reclen - uint64(unsafe.Offsetof(syscall.Dirent{}.Name)), true +} + +func direntType(buf []byte) FileMode { + return ^FileMode(0) // unknown +} diff --git a/src/os/dirent_wasip1.go b/src/os/dirent_wasip1.go new file mode 100644 index 0000000..d3f10b2 --- /dev/null +++ b/src/os/dirent_wasip1.go @@ -0,0 +1,52 @@ +// 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. + +//go:build wasip1 + +package os + +import ( + "syscall" + "unsafe" +) + +// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#-dirent-record +const sizeOfDirent = 24 + +func direntIno(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Ino), unsafe.Sizeof(syscall.Dirent{}.Ino)) +} + +func direntReclen(buf []byte) (uint64, bool) { + namelen, ok := direntNamlen(buf) + return sizeOfDirent + namelen, ok +} + +func direntNamlen(buf []byte) (uint64, bool) { + return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Namlen), unsafe.Sizeof(syscall.Dirent{}.Namlen)) +} + +func direntType(buf []byte) FileMode { + off := unsafe.Offsetof(syscall.Dirent{}.Type) + if off >= uintptr(len(buf)) { + return ^FileMode(0) // unknown + } + switch syscall.Filetype(buf[off]) { + case syscall.FILETYPE_BLOCK_DEVICE: + return ModeDevice + case syscall.FILETYPE_CHARACTER_DEVICE: + return ModeDevice | ModeCharDevice + case syscall.FILETYPE_DIRECTORY: + return ModeDir + case syscall.FILETYPE_REGULAR_FILE: + return 0 + case syscall.FILETYPE_SOCKET_DGRAM: + return ModeSocket + case syscall.FILETYPE_SOCKET_STREAM: + return ModeSocket + case syscall.FILETYPE_SYMBOLIC_LINK: + return ModeSymlink + } + return ^FileMode(0) // unknown +} diff --git a/src/os/endian_big.go b/src/os/endian_big.go new file mode 100644 index 0000000..0375e53 --- /dev/null +++ b/src/os/endian_big.go @@ -0,0 +1,9 @@ +// Copyright 2016 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. +// +//go:build ppc64 || s390x || mips || mips64 + +package os + +const isBigEndian = true diff --git a/src/os/endian_little.go b/src/os/endian_little.go new file mode 100644 index 0000000..a7cf1cd --- /dev/null +++ b/src/os/endian_little.go @@ -0,0 +1,9 @@ +// Copyright 2016 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. +// +//go:build 386 || amd64 || arm || arm64 || loong64 || ppc64le || mips64le || mipsle || riscv64 || wasm + +package os + +const isBigEndian = false diff --git a/src/os/env.go b/src/os/env.go new file mode 100644 index 0000000..63ad5ab --- /dev/null +++ b/src/os/env.go @@ -0,0 +1,141 @@ +// Copyright 2010 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. + +// General environment variables. + +package os + +import ( + "internal/testlog" + "syscall" +) + +// Expand replaces ${var} or $var in the string based on the mapping function. +// For example, os.ExpandEnv(s) is equivalent to os.Expand(s, os.Getenv). +func Expand(s string, mapping func(string) string) string { + var buf []byte + // ${} is all ASCII, so bytes are fine for this operation. + i := 0 + for j := 0; j < len(s); j++ { + if s[j] == '$' && j+1 < len(s) { + if buf == nil { + buf = make([]byte, 0, 2*len(s)) + } + buf = append(buf, s[i:j]...) + name, w := getShellName(s[j+1:]) + if name == "" && w > 0 { + // Encountered invalid syntax; eat the + // characters. + } else if name == "" { + // Valid syntax, but $ was not followed by a + // name. Leave the dollar character untouched. + buf = append(buf, s[j]) + } else { + buf = append(buf, mapping(name)...) + } + j += w + i = j + 1 + } + } + if buf == nil { + return s + } + return string(buf) + s[i:] +} + +// ExpandEnv replaces ${var} or $var in the string according to the values +// of the current environment variables. References to undefined +// variables are replaced by the empty string. +func ExpandEnv(s string) string { + return Expand(s, Getenv) +} + +// isShellSpecialVar reports whether the character identifies a special +// shell variable such as $*. +func isShellSpecialVar(c uint8) bool { + switch c { + case '*', '#', '$', '@', '!', '?', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + return true + } + return false +} + +// isAlphaNum reports whether the byte is an ASCII letter, number, or underscore. +func isAlphaNum(c uint8) bool { + return c == '_' || '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' +} + +// getShellName returns the name that begins the string and the number of bytes +// consumed to extract it. If the name is enclosed in {}, it's part of a ${} +// expansion and two more bytes are needed than the length of the name. +func getShellName(s string) (string, int) { + switch { + case s[0] == '{': + if len(s) > 2 && isShellSpecialVar(s[1]) && s[2] == '}' { + return s[1:2], 3 + } + // Scan to closing brace + for i := 1; i < len(s); i++ { + if s[i] == '}' { + if i == 1 { + return "", 2 // Bad syntax; eat "${}" + } + return s[1:i], i + 1 + } + } + return "", 1 // Bad syntax; eat "${" + case isShellSpecialVar(s[0]): + return s[0:1], 1 + } + // Scan alphanumerics. + var i int + for i = 0; i < len(s) && isAlphaNum(s[i]); i++ { + } + return s[:i], i +} + +// Getenv retrieves the value of the environment variable named by the key. +// It returns the value, which will be empty if the variable is not present. +// To distinguish between an empty value and an unset value, use LookupEnv. +func Getenv(key string) string { + testlog.Getenv(key) + v, _ := syscall.Getenv(key) + return v +} + +// LookupEnv retrieves the value of the environment variable named +// by the key. If the variable is present in the environment the +// value (which may be empty) is returned and the boolean is true. +// Otherwise the returned value will be empty and the boolean will +// be false. +func LookupEnv(key string) (string, bool) { + testlog.Getenv(key) + return syscall.Getenv(key) +} + +// Setenv sets the value of the environment variable named by the key. +// It returns an error, if any. +func Setenv(key, value string) error { + err := syscall.Setenv(key, value) + if err != nil { + return NewSyscallError("setenv", err) + } + return nil +} + +// Unsetenv unsets a single environment variable. +func Unsetenv(key string) error { + return syscall.Unsetenv(key) +} + +// Clearenv deletes all environment variables. +func Clearenv() { + syscall.Clearenv() +} + +// Environ returns a copy of strings representing the environment, +// in the form "key=value". +func Environ() []string { + return syscall.Environ() +} diff --git a/src/os/env_test.go b/src/os/env_test.go new file mode 100644 index 0000000..5809f4b --- /dev/null +++ b/src/os/env_test.go @@ -0,0 +1,206 @@ +// Copyright 2010 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 os_test + +import ( + . "os" + "reflect" + "strings" + "testing" +) + +// testGetenv gives us a controlled set of variables for testing Expand. +func testGetenv(s string) string { + switch s { + case "*": + return "all the args" + case "#": + return "NARGS" + case "$": + return "PID" + case "1": + return "ARGUMENT1" + case "HOME": + return "/usr/gopher" + case "H": + return "(Value of H)" + case "home_1": + return "/usr/foo" + case "_": + return "underscore" + } + return "" +} + +var expandTests = []struct { + in, out string +}{ + {"", ""}, + {"$*", "all the args"}, + {"$$", "PID"}, + {"${*}", "all the args"}, + {"$1", "ARGUMENT1"}, + {"${1}", "ARGUMENT1"}, + {"now is the time", "now is the time"}, + {"$HOME", "/usr/gopher"}, + {"$home_1", "/usr/foo"}, + {"${HOME}", "/usr/gopher"}, + {"${H}OME", "(Value of H)OME"}, + {"A$$$#$1$H$home_1*B", "APIDNARGSARGUMENT1(Value of H)/usr/foo*B"}, + {"start$+middle$^end$", "start$+middle$^end$"}, + {"mixed$|bag$$$", "mixed$|bagPID$"}, + {"$", "$"}, + {"$}", "$}"}, + {"${", ""}, // invalid syntax; eat up the characters + {"${}", ""}, // invalid syntax; eat up the characters +} + +func TestExpand(t *testing.T) { + for _, test := range expandTests { + result := Expand(test.in, testGetenv) + if result != test.out { + t.Errorf("Expand(%q)=%q; expected %q", test.in, result, test.out) + } + } +} + +var global any + +func BenchmarkExpand(b *testing.B) { + b.Run("noop", func(b *testing.B) { + var s string + b.ReportAllocs() + for i := 0; i < b.N; i++ { + s = Expand("tick tick tick tick", func(string) string { return "" }) + } + global = s + }) + b.Run("multiple", func(b *testing.B) { + var s string + b.ReportAllocs() + for i := 0; i < b.N; i++ { + s = Expand("$a $a $a $a", func(string) string { return "boom" }) + } + global = s + }) +} + +func TestConsistentEnviron(t *testing.T) { + e0 := Environ() + for i := 0; i < 10; i++ { + e1 := Environ() + if !reflect.DeepEqual(e0, e1) { + t.Fatalf("environment changed") + } + } +} + +func TestUnsetenv(t *testing.T) { + const testKey = "GO_TEST_UNSETENV" + set := func() bool { + prefix := testKey + "=" + for _, key := range Environ() { + if strings.HasPrefix(key, prefix) { + return true + } + } + return false + } + if err := Setenv(testKey, "1"); err != nil { + t.Fatalf("Setenv: %v", err) + } + if !set() { + t.Error("Setenv didn't set TestUnsetenv") + } + if err := Unsetenv(testKey); err != nil { + t.Fatalf("Unsetenv: %v", err) + } + if set() { + t.Fatal("Unsetenv didn't clear TestUnsetenv") + } +} + +func TestClearenv(t *testing.T) { + const testKey = "GO_TEST_CLEARENV" + const testValue = "1" + + // reset env + defer func(origEnv []string) { + for _, pair := range origEnv { + // Environment variables on Windows can begin with = + // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133 + i := strings.Index(pair[1:], "=") + 1 + if err := Setenv(pair[:i], pair[i+1:]); err != nil { + t.Errorf("Setenv(%q, %q) failed during reset: %v", pair[:i], pair[i+1:], err) + } + } + }(Environ()) + + if err := Setenv(testKey, testValue); err != nil { + t.Fatalf("Setenv(%q, %q) failed: %v", testKey, testValue, err) + } + if _, ok := LookupEnv(testKey); !ok { + t.Errorf("Setenv(%q, %q) didn't set $%s", testKey, testValue, testKey) + } + Clearenv() + if val, ok := LookupEnv(testKey); ok { + t.Errorf("Clearenv() didn't clear $%s, remained with value %q", testKey, val) + } +} + +func TestLookupEnv(t *testing.T) { + const smallpox = "SMALLPOX" // No one has smallpox. + value, ok := LookupEnv(smallpox) // Should not exist. + if ok || value != "" { + t.Fatalf("%s=%q", smallpox, value) + } + defer Unsetenv(smallpox) + err := Setenv(smallpox, "virus") + if err != nil { + t.Fatalf("failed to release smallpox virus") + } + _, ok = LookupEnv(smallpox) + if !ok { + t.Errorf("smallpox release failed; world remains safe but LookupEnv is broken") + } +} + +// On Windows, Environ was observed to report keys with a single leading "=". +// Check that they are properly reported by LookupEnv and can be set by SetEnv. +// See https://golang.org/issue/49886. +func TestEnvironConsistency(t *testing.T) { + t.Parallel() + + for _, kv := range Environ() { + i := strings.Index(kv, "=") + if i == 0 { + // We observe in practice keys with a single leading "=" on Windows. + // TODO(#49886): Should we consume only the first leading "=" as part + // of the key, or parse through arbitrarily many of them until a non-=, + // or try each possible key/value boundary until LookupEnv succeeds? + i = strings.Index(kv[1:], "=") + 1 + } + if i < 0 { + t.Errorf("Environ entry missing '=': %q", kv) + } + + k := kv[:i] + v := kv[i+1:] + v2, ok := LookupEnv(k) + if ok && v == v2 { + t.Logf("LookupEnv(%q) = %q, %t", k, v2, ok) + } else { + t.Errorf("Environ contains %q, but LookupEnv(%q) = %q, %t", kv, k, v2, ok) + } + + // Since k=v is already present in the environment, + // setting it should be a no-op. + if err := Setenv(k, v); err == nil { + t.Logf("Setenv(%q, %q)", k, v) + } else { + t.Errorf("Environ contains %q, but SetEnv(%q, %q) = %q", kv, k, v, err) + } + } +} diff --git a/src/os/env_unix_test.go b/src/os/env_unix_test.go new file mode 100644 index 0000000..4609fc3 --- /dev/null +++ b/src/os/env_unix_test.go @@ -0,0 +1,56 @@ +// Copyright 2013 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. + +//go:build unix + +package os_test + +import ( + "fmt" + . "os" + "testing" +) + +var setenvEinvalTests = []struct { + k, v string +}{ + {"", ""}, // empty key + {"k=v", ""}, // '=' in key + {"\x00", ""}, // '\x00' in key + {"k", "\x00"}, // '\x00' in value +} + +func TestSetenvUnixEinval(t *testing.T) { + for _, tt := range setenvEinvalTests { + err := Setenv(tt.k, tt.v) + if err == nil { + t.Errorf(`Setenv(%q, %q) == nil, want error`, tt.k, tt.v) + } + } +} + +var shellSpecialVarTests = []struct { + k, v string +}{ + {"*", "asterisk"}, + {"#", "pound"}, + {"$", "dollar"}, + {"@", "at"}, + {"!", "exclamation mark"}, + {"?", "question mark"}, + {"-", "dash"}, +} + +func TestExpandEnvShellSpecialVar(t *testing.T) { + for _, tt := range shellSpecialVarTests { + Setenv(tt.k, tt.v) + defer Unsetenv(tt.k) + + argRaw := fmt.Sprintf("$%s", tt.k) + argWithBrace := fmt.Sprintf("${%s}", tt.k) + if gotRaw, gotBrace := ExpandEnv(argRaw), ExpandEnv(argWithBrace); gotRaw != gotBrace { + t.Errorf("ExpandEnv(%q) = %q, ExpandEnv(%q) = %q; expect them to be equal", argRaw, gotRaw, argWithBrace, gotBrace) + } + } +} diff --git a/src/os/error.go b/src/os/error.go new file mode 100644 index 0000000..62ede9d --- /dev/null +++ b/src/os/error.go @@ -0,0 +1,141 @@ +// Copyright 2009 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 os + +import ( + "internal/poll" + "io/fs" +) + +// Portable analogs of some common system call errors. +// +// Errors returned from this package may be tested against these errors +// with errors.Is. +var ( + // ErrInvalid indicates an invalid argument. + // Methods on File will return this error when the receiver is nil. + ErrInvalid = fs.ErrInvalid // "invalid argument" + + ErrPermission = fs.ErrPermission // "permission denied" + ErrExist = fs.ErrExist // "file already exists" + ErrNotExist = fs.ErrNotExist // "file does not exist" + ErrClosed = fs.ErrClosed // "file already closed" + + ErrNoDeadline = errNoDeadline() // "file type does not support deadline" + ErrDeadlineExceeded = errDeadlineExceeded() // "i/o timeout" +) + +func errNoDeadline() error { return poll.ErrNoDeadline } + +// errDeadlineExceeded returns the value for os.ErrDeadlineExceeded. +// This error comes from the internal/poll package, which is also +// used by package net. Doing it this way ensures that the net +// package will return os.ErrDeadlineExceeded for an exceeded deadline, +// as documented by net.Conn.SetDeadline, without requiring any extra +// work in the net package and without requiring the internal/poll +// package to import os (which it can't, because that would be circular). +func errDeadlineExceeded() error { return poll.ErrDeadlineExceeded } + +type timeout interface { + Timeout() bool +} + +// PathError records an error and the operation and file path that caused it. +type PathError = fs.PathError + +// SyscallError records an error from a specific system call. +type SyscallError struct { + Syscall string + Err error +} + +func (e *SyscallError) Error() string { return e.Syscall + ": " + e.Err.Error() } + +func (e *SyscallError) Unwrap() error { return e.Err } + +// Timeout reports whether this error represents a timeout. +func (e *SyscallError) Timeout() bool { + t, ok := e.Err.(timeout) + return ok && t.Timeout() +} + +// NewSyscallError returns, as an error, a new SyscallError +// with the given system call name and error details. +// As a convenience, if err is nil, NewSyscallError returns nil. +func NewSyscallError(syscall string, err error) error { + if err == nil { + return nil + } + return &SyscallError{syscall, err} +} + +// IsExist returns a boolean indicating whether the error is known to report +// that a file or directory already exists. It is satisfied by ErrExist as +// well as some syscall errors. +// +// This function predates errors.Is. It only supports errors returned by +// the os package. New code should use errors.Is(err, fs.ErrExist). +func IsExist(err error) bool { + return underlyingErrorIs(err, ErrExist) +} + +// IsNotExist returns a boolean indicating whether the error is known to +// report that a file or directory does not exist. It is satisfied by +// ErrNotExist as well as some syscall errors. +// +// This function predates errors.Is. It only supports errors returned by +// the os package. New code should use errors.Is(err, fs.ErrNotExist). +func IsNotExist(err error) bool { + return underlyingErrorIs(err, ErrNotExist) +} + +// IsPermission returns a boolean indicating whether the error is known to +// report that permission is denied. It is satisfied by ErrPermission as well +// as some syscall errors. +// +// This function predates errors.Is. It only supports errors returned by +// the os package. New code should use errors.Is(err, fs.ErrPermission). +func IsPermission(err error) bool { + return underlyingErrorIs(err, ErrPermission) +} + +// IsTimeout returns a boolean indicating whether the error is known +// to report that a timeout occurred. +// +// This function predates errors.Is, and the notion of whether an +// error indicates a timeout can be ambiguous. For example, the Unix +// error EWOULDBLOCK sometimes indicates a timeout and sometimes does not. +// New code should use errors.Is with a value appropriate to the call +// returning the error, such as os.ErrDeadlineExceeded. +func IsTimeout(err error) bool { + terr, ok := underlyingError(err).(timeout) + return ok && terr.Timeout() +} + +func underlyingErrorIs(err, target error) bool { + // Note that this function is not errors.Is: + // underlyingError only unwraps the specific error-wrapping types + // that it historically did, not all errors implementing Unwrap(). + err = underlyingError(err) + if err == target { + return true + } + // To preserve prior behavior, only examine syscall errors. + e, ok := err.(syscallErrorType) + return ok && e.Is(target) +} + +// underlyingError returns the underlying error for known os error types. +func underlyingError(err error) error { + switch err := err.(type) { + case *PathError: + return err.Err + case *LinkError: + return err.Err + case *SyscallError: + return err.Err + } + return err +} diff --git a/src/os/error_errno.go b/src/os/error_errno.go new file mode 100644 index 0000000..c814046 --- /dev/null +++ b/src/os/error_errno.go @@ -0,0 +1,11 @@ +// Copyright 2019 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. + +//go:build !plan9 + +package os + +import "syscall" + +type syscallErrorType = syscall.Errno diff --git a/src/os/error_plan9.go b/src/os/error_plan9.go new file mode 100644 index 0000000..af6065d --- /dev/null +++ b/src/os/error_plan9.go @@ -0,0 +1,9 @@ +// Copyright 2019 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 os + +import "syscall" + +type syscallErrorType = syscall.ErrorString diff --git a/src/os/error_posix.go b/src/os/error_posix.go new file mode 100644 index 0000000..b159c03 --- /dev/null +++ b/src/os/error_posix.go @@ -0,0 +1,18 @@ +// Copyright 2017 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. + +//go:build unix || (js && wasm) || wasip1 || windows + +package os + +import "syscall" + +// wrapSyscallError takes an error and a syscall name. If the error is +// a syscall.Errno, it wraps it in an os.SyscallError using the syscall name. +func wrapSyscallError(name string, err error) error { + if _, ok := err.(syscall.Errno); ok { + err = NewSyscallError(name, err) + } + return err +} diff --git a/src/os/error_test.go b/src/os/error_test.go new file mode 100644 index 0000000..8f82ae6 --- /dev/null +++ b/src/os/error_test.go @@ -0,0 +1,189 @@ +// Copyright 2012 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 os_test + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "testing" +) + +func TestErrIsExist(t *testing.T) { + t.Parallel() + + f, err := os.CreateTemp("", "_Go_ErrIsExist") + if err != nil { + t.Fatalf("open ErrIsExist tempfile: %s", err) + return + } + defer os.Remove(f.Name()) + defer f.Close() + f2, err := os.OpenFile(f.Name(), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) + if err == nil { + f2.Close() + t.Fatal("Open should have failed") + } + if s := checkErrorPredicate("os.IsExist", os.IsExist, err, fs.ErrExist); s != "" { + t.Fatal(s) + } +} + +func testErrNotExist(t *testing.T, name string) string { + originalWD, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + f, err := os.Open(name) + if err == nil { + f.Close() + return "Open should have failed" + } + if s := checkErrorPredicate("os.IsNotExist", os.IsNotExist, err, fs.ErrNotExist); s != "" { + return s + } + + err = os.Chdir(name) + if err == nil { + if err := os.Chdir(originalWD); err != nil { + t.Fatalf("Chdir should have failed, failed to restore original working directory: %v", err) + } + return "Chdir should have failed, restored original working directory" + } + if s := checkErrorPredicate("os.IsNotExist", os.IsNotExist, err, fs.ErrNotExist); s != "" { + return s + } + return "" +} + +func TestErrIsNotExist(t *testing.T) { + tmpDir := t.TempDir() + name := filepath.Join(tmpDir, "NotExists") + if s := testErrNotExist(t, name); s != "" { + t.Fatal(s) + } + + name = filepath.Join(name, "NotExists2") + if s := testErrNotExist(t, name); s != "" { + t.Fatal(s) + } +} + +func checkErrorPredicate(predName string, pred func(error) bool, err, target error) string { + if !pred(err) { + return fmt.Sprintf("%s does not work as expected for %#v", predName, err) + } + if !errors.Is(err, target) { + return fmt.Sprintf("errors.Is(%#v, %#v) = false, want true", err, target) + } + return "" +} + +type isExistTest struct { + err error + is bool + isnot bool +} + +var isExistTests = []isExistTest{ + {&fs.PathError{Err: fs.ErrInvalid}, false, false}, + {&fs.PathError{Err: fs.ErrPermission}, false, false}, + {&fs.PathError{Err: fs.ErrExist}, true, false}, + {&fs.PathError{Err: fs.ErrNotExist}, false, true}, + {&fs.PathError{Err: fs.ErrClosed}, false, false}, + {&os.LinkError{Err: fs.ErrInvalid}, false, false}, + {&os.LinkError{Err: fs.ErrPermission}, false, false}, + {&os.LinkError{Err: fs.ErrExist}, true, false}, + {&os.LinkError{Err: fs.ErrNotExist}, false, true}, + {&os.LinkError{Err: fs.ErrClosed}, false, false}, + {&os.SyscallError{Err: fs.ErrNotExist}, false, true}, + {&os.SyscallError{Err: fs.ErrExist}, true, false}, + {nil, false, false}, +} + +func TestIsExist(t *testing.T) { + for _, tt := range isExistTests { + if is := os.IsExist(tt.err); is != tt.is { + t.Errorf("os.IsExist(%T %v) = %v, want %v", tt.err, tt.err, is, tt.is) + } + if is := errors.Is(tt.err, fs.ErrExist); is != tt.is { + t.Errorf("errors.Is(%T %v, fs.ErrExist) = %v, want %v", tt.err, tt.err, is, tt.is) + } + if isnot := os.IsNotExist(tt.err); isnot != tt.isnot { + t.Errorf("os.IsNotExist(%T %v) = %v, want %v", tt.err, tt.err, isnot, tt.isnot) + } + if isnot := errors.Is(tt.err, fs.ErrNotExist); isnot != tt.isnot { + t.Errorf("errors.Is(%T %v, fs.ErrNotExist) = %v, want %v", tt.err, tt.err, isnot, tt.isnot) + } + } +} + +type isPermissionTest struct { + err error + want bool +} + +var isPermissionTests = []isPermissionTest{ + {nil, false}, + {&fs.PathError{Err: fs.ErrPermission}, true}, + {&os.SyscallError{Err: fs.ErrPermission}, true}, +} + +func TestIsPermission(t *testing.T) { + for _, tt := range isPermissionTests { + if got := os.IsPermission(tt.err); got != tt.want { + t.Errorf("os.IsPermission(%#v) = %v; want %v", tt.err, got, tt.want) + } + if got := errors.Is(tt.err, fs.ErrPermission); got != tt.want { + t.Errorf("errors.Is(%#v, fs.ErrPermission) = %v; want %v", tt.err, got, tt.want) + } + } +} + +func TestErrPathNUL(t *testing.T) { + t.Parallel() + + f, err := os.CreateTemp("", "_Go_ErrPathNUL\x00") + if err == nil { + f.Close() + t.Fatal("TempFile should have failed") + } + f, err = os.CreateTemp("", "_Go_ErrPathNUL") + if err != nil { + t.Fatalf("open ErrPathNUL tempfile: %s", err) + } + defer os.Remove(f.Name()) + defer f.Close() + f2, err := os.OpenFile(f.Name(), os.O_RDWR, 0600) + if err != nil { + t.Fatalf("open ErrPathNUL: %s", err) + } + f2.Close() + f2, err = os.OpenFile(f.Name()+"\x00", os.O_RDWR, 0600) + if err == nil { + f2.Close() + t.Fatal("Open should have failed") + } +} + +func TestPathErrorUnwrap(t *testing.T) { + pe := &fs.PathError{Err: fs.ErrInvalid} + if !errors.Is(pe, fs.ErrInvalid) { + t.Error("errors.Is failed, wanted success") + } +} + +type myErrorIs struct{ error } + +func (e myErrorIs) Is(target error) bool { return target == e.error } + +func TestErrorIsMethods(t *testing.T) { + if os.IsPermission(myErrorIs{fs.ErrPermission}) { + t.Error("os.IsPermission(err) = true when err.Is(fs.ErrPermission), wanted false") + } +} diff --git a/src/os/error_unix_test.go b/src/os/error_unix_test.go new file mode 100644 index 0000000..07a3286 --- /dev/null +++ b/src/os/error_unix_test.go @@ -0,0 +1,40 @@ +// Copyright 2016 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. + +//go:build unix || (js && wasm) || wasip1 + +package os_test + +import ( + "io/fs" + "os" + "syscall" +) + +func init() { + isExistTests = append(isExistTests, + isExistTest{err: &fs.PathError{Err: syscall.EEXIST}, is: true, isnot: false}, + isExistTest{err: &fs.PathError{Err: syscall.ENOTEMPTY}, is: true, isnot: false}, + + isExistTest{err: &os.LinkError{Err: syscall.EEXIST}, is: true, isnot: false}, + isExistTest{err: &os.LinkError{Err: syscall.ENOTEMPTY}, is: true, isnot: false}, + + isExistTest{err: &os.SyscallError{Err: syscall.EEXIST}, is: true, isnot: false}, + isExistTest{err: &os.SyscallError{Err: syscall.ENOTEMPTY}, is: true, isnot: false}, + ) + isPermissionTests = append(isPermissionTests, + isPermissionTest{err: &fs.PathError{Err: syscall.EACCES}, want: true}, + isPermissionTest{err: &fs.PathError{Err: syscall.EPERM}, want: true}, + isPermissionTest{err: &fs.PathError{Err: syscall.EEXIST}, want: false}, + + isPermissionTest{err: &os.LinkError{Err: syscall.EACCES}, want: true}, + isPermissionTest{err: &os.LinkError{Err: syscall.EPERM}, want: true}, + isPermissionTest{err: &os.LinkError{Err: syscall.EEXIST}, want: false}, + + isPermissionTest{err: &os.SyscallError{Err: syscall.EACCES}, want: true}, + isPermissionTest{err: &os.SyscallError{Err: syscall.EPERM}, want: true}, + isPermissionTest{err: &os.SyscallError{Err: syscall.EEXIST}, want: false}, + ) + +} diff --git a/src/os/error_windows_test.go b/src/os/error_windows_test.go new file mode 100644 index 0000000..86c8a98 --- /dev/null +++ b/src/os/error_windows_test.go @@ -0,0 +1,40 @@ +// Copyright 2016 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. + +//go:build windows + +package os_test + +import ( + "io/fs" + "os" + "syscall" +) + +func init() { + const _ERROR_BAD_NETPATH = syscall.Errno(53) + + isExistTests = append(isExistTests, + isExistTest{err: &fs.PathError{Err: syscall.ERROR_FILE_NOT_FOUND}, is: false, isnot: true}, + isExistTest{err: &os.LinkError{Err: syscall.ERROR_FILE_NOT_FOUND}, is: false, isnot: true}, + isExistTest{err: &os.SyscallError{Err: syscall.ERROR_FILE_NOT_FOUND}, is: false, isnot: true}, + + isExistTest{err: &fs.PathError{Err: _ERROR_BAD_NETPATH}, is: false, isnot: true}, + isExistTest{err: &os.LinkError{Err: _ERROR_BAD_NETPATH}, is: false, isnot: true}, + isExistTest{err: &os.SyscallError{Err: _ERROR_BAD_NETPATH}, is: false, isnot: true}, + + isExistTest{err: &fs.PathError{Err: syscall.ERROR_PATH_NOT_FOUND}, is: false, isnot: true}, + isExistTest{err: &os.LinkError{Err: syscall.ERROR_PATH_NOT_FOUND}, is: false, isnot: true}, + isExistTest{err: &os.SyscallError{Err: syscall.ERROR_PATH_NOT_FOUND}, is: false, isnot: true}, + + isExistTest{err: &fs.PathError{Err: syscall.ERROR_DIR_NOT_EMPTY}, is: true, isnot: false}, + isExistTest{err: &os.LinkError{Err: syscall.ERROR_DIR_NOT_EMPTY}, is: true, isnot: false}, + isExistTest{err: &os.SyscallError{Err: syscall.ERROR_DIR_NOT_EMPTY}, is: true, isnot: false}, + ) + isPermissionTests = append(isPermissionTests, + isPermissionTest{err: &fs.PathError{Err: syscall.ERROR_ACCESS_DENIED}, want: true}, + isPermissionTest{err: &os.LinkError{Err: syscall.ERROR_ACCESS_DENIED}, want: true}, + isPermissionTest{err: &os.SyscallError{Err: syscall.ERROR_ACCESS_DENIED}, want: true}, + ) +} diff --git a/src/os/example_test.go b/src/os/example_test.go new file mode 100644 index 0000000..5c7c6ea --- /dev/null +++ b/src/os/example_test.go @@ -0,0 +1,265 @@ +// Copyright 2016 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 os_test + +import ( + "errors" + "fmt" + "io/fs" + "log" + "os" + "path/filepath" + "time" +) + +func ExampleOpenFile() { + f, err := os.OpenFile("notes.txt", os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + log.Fatal(err) + } + if err := f.Close(); err != nil { + log.Fatal(err) + } +} + +func ExampleOpenFile_append() { + // If the file doesn't exist, create it, or append to the file + f, err := os.OpenFile("access.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Fatal(err) + } + if _, err := f.Write([]byte("appended some data\n")); err != nil { + f.Close() // ignore error; Write error takes precedence + log.Fatal(err) + } + if err := f.Close(); err != nil { + log.Fatal(err) + } +} + +func ExampleChmod() { + if err := os.Chmod("some-filename", 0644); err != nil { + log.Fatal(err) + } +} + +func ExampleChtimes() { + mtime := time.Date(2006, time.February, 1, 3, 4, 5, 0, time.UTC) + atime := time.Date(2007, time.March, 2, 4, 5, 6, 0, time.UTC) + if err := os.Chtimes("some-filename", atime, mtime); err != nil { + log.Fatal(err) + } +} + +func ExampleFileMode() { + fi, err := os.Lstat("some-filename") + if err != nil { + log.Fatal(err) + } + + fmt.Printf("permissions: %#o\n", fi.Mode().Perm()) // 0400, 0777, etc. + switch mode := fi.Mode(); { + case mode.IsRegular(): + fmt.Println("regular file") + case mode.IsDir(): + fmt.Println("directory") + case mode&fs.ModeSymlink != 0: + fmt.Println("symbolic link") + case mode&fs.ModeNamedPipe != 0: + fmt.Println("named pipe") + } +} + +func ExampleErrNotExist() { + filename := "a-nonexistent-file" + if _, err := os.Stat(filename); errors.Is(err, fs.ErrNotExist) { + fmt.Println("file does not exist") + } + // Output: + // file does not exist +} + +func ExampleExpand() { + mapper := func(placeholderName string) string { + switch placeholderName { + case "DAY_PART": + return "morning" + case "NAME": + return "Gopher" + } + + return "" + } + + fmt.Println(os.Expand("Good ${DAY_PART}, $NAME!", mapper)) + + // Output: + // Good morning, Gopher! +} + +func ExampleExpandEnv() { + os.Setenv("NAME", "gopher") + os.Setenv("BURROW", "/usr/gopher") + + fmt.Println(os.ExpandEnv("$NAME lives in ${BURROW}.")) + + // Output: + // gopher lives in /usr/gopher. +} + +func ExampleLookupEnv() { + show := func(key string) { + val, ok := os.LookupEnv(key) + if !ok { + fmt.Printf("%s not set\n", key) + } else { + fmt.Printf("%s=%s\n", key, val) + } + } + + os.Setenv("SOME_KEY", "value") + os.Setenv("EMPTY_KEY", "") + + show("SOME_KEY") + show("EMPTY_KEY") + show("MISSING_KEY") + + // Output: + // SOME_KEY=value + // EMPTY_KEY= + // MISSING_KEY not set +} + +func ExampleGetenv() { + os.Setenv("NAME", "gopher") + os.Setenv("BURROW", "/usr/gopher") + + fmt.Printf("%s lives in %s.\n", os.Getenv("NAME"), os.Getenv("BURROW")) + + // Output: + // gopher lives in /usr/gopher. +} + +func ExampleUnsetenv() { + os.Setenv("TMPDIR", "/my/tmp") + defer os.Unsetenv("TMPDIR") +} + +func ExampleReadDir() { + files, err := os.ReadDir(".") + if err != nil { + log.Fatal(err) + } + + for _, file := range files { + fmt.Println(file.Name()) + } +} + +func ExampleMkdirTemp() { + dir, err := os.MkdirTemp("", "example") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(dir) // clean up + + file := filepath.Join(dir, "tmpfile") + if err := os.WriteFile(file, []byte("content"), 0666); err != nil { + log.Fatal(err) + } +} + +func ExampleMkdirTemp_suffix() { + logsDir, err := os.MkdirTemp("", "*-logs") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(logsDir) // clean up + + // Logs can be cleaned out earlier if needed by searching + // for all directories whose suffix ends in *-logs. + globPattern := filepath.Join(os.TempDir(), "*-logs") + matches, err := filepath.Glob(globPattern) + if err != nil { + log.Fatalf("Failed to match %q: %v", globPattern, err) + } + + for _, match := range matches { + if err := os.RemoveAll(match); err != nil { + log.Printf("Failed to remove %q: %v", match, err) + } + } +} + +func ExampleCreateTemp() { + f, err := os.CreateTemp("", "example") + if err != nil { + log.Fatal(err) + } + defer os.Remove(f.Name()) // clean up + + if _, err := f.Write([]byte("content")); err != nil { + log.Fatal(err) + } + if err := f.Close(); err != nil { + log.Fatal(err) + } +} + +func ExampleCreateTemp_suffix() { + f, err := os.CreateTemp("", "example.*.txt") + if err != nil { + log.Fatal(err) + } + defer os.Remove(f.Name()) // clean up + + if _, err := f.Write([]byte("content")); err != nil { + f.Close() + log.Fatal(err) + } + if err := f.Close(); err != nil { + log.Fatal(err) + } +} + +func ExampleReadFile() { + data, err := os.ReadFile("testdata/hello") + if err != nil { + log.Fatal(err) + } + os.Stdout.Write(data) + + // Output: + // Hello, Gophers! +} + +func ExampleWriteFile() { + err := os.WriteFile("testdata/hello", []byte("Hello, Gophers!"), 0666) + if err != nil { + log.Fatal(err) + } +} + +func ExampleMkdir() { + err := os.Mkdir("testdir", 0750) + if err != nil && !os.IsExist(err) { + log.Fatal(err) + } + err = os.WriteFile("testdir/testfile.txt", []byte("Hello, Gophers!"), 0660) + if err != nil { + log.Fatal(err) + } +} + +func ExampleMkdirAll() { + err := os.MkdirAll("test/subdir", 0750) + if err != nil { + log.Fatal(err) + } + err = os.WriteFile("test/subdir/testfile.txt", []byte("Hello, Gophers!"), 0660) + if err != nil { + log.Fatal(err) + } +} diff --git a/src/os/exec.go b/src/os/exec.go new file mode 100644 index 0000000..ed5a75c --- /dev/null +++ b/src/os/exec.go @@ -0,0 +1,180 @@ +// Copyright 2009 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 os + +import ( + "errors" + "internal/testlog" + "runtime" + "sync" + "sync/atomic" + "syscall" + "time" +) + +// ErrProcessDone indicates a Process has finished. +var ErrProcessDone = errors.New("os: process already finished") + +// Process stores the information about a process created by StartProcess. +type Process struct { + Pid int + handle uintptr // handle is accessed atomically on Windows + isdone atomic.Bool // process has been successfully waited on + sigMu sync.RWMutex // avoid race between wait and signal +} + +func newProcess(pid int, handle uintptr) *Process { + p := &Process{Pid: pid, handle: handle} + runtime.SetFinalizer(p, (*Process).Release) + return p +} + +func (p *Process) setDone() { + p.isdone.Store(true) +} + +func (p *Process) done() bool { + return p.isdone.Load() +} + +// ProcAttr holds the attributes that will be applied to a new process +// started by StartProcess. +type ProcAttr struct { + // If Dir is non-empty, the child changes into the directory before + // creating the process. + Dir string + // If Env is non-nil, it gives the environment variables for the + // new process in the form returned by Environ. + // If it is nil, the result of Environ will be used. + Env []string + // Files specifies the open files inherited by the new process. The + // first three entries correspond to standard input, standard output, and + // standard error. An implementation may support additional entries, + // depending on the underlying operating system. A nil entry corresponds + // to that file being closed when the process starts. + // On Unix systems, StartProcess will change these File values + // to blocking mode, which means that SetDeadline will stop working + // and calling Close will not interrupt a Read or Write. + Files []*File + + // Operating system-specific process creation attributes. + // Note that setting this field means that your program + // may not execute properly or even compile on some + // operating systems. + Sys *syscall.SysProcAttr +} + +// A Signal represents an operating system signal. +// The usual underlying implementation is operating system-dependent: +// on Unix it is syscall.Signal. +type Signal interface { + String() string + Signal() // to distinguish from other Stringers +} + +// Getpid returns the process id of the caller. +func Getpid() int { return syscall.Getpid() } + +// Getppid returns the process id of the caller's parent. +func Getppid() int { return syscall.Getppid() } + +// FindProcess looks for a running process by its pid. +// +// The Process it returns can be used to obtain information +// about the underlying operating system process. +// +// On Unix systems, FindProcess always succeeds and returns a Process +// for the given pid, regardless of whether the process exists. To test whether +// the process actually exists, see whether p.Signal(syscall.Signal(0)) reports +// an error. +func FindProcess(pid int) (*Process, error) { + return findProcess(pid) +} + +// StartProcess starts a new process with the program, arguments and attributes +// specified by name, argv and attr. The argv slice will become os.Args in the +// new process, so it normally starts with the program name. +// +// If the calling goroutine has locked the operating system thread +// with runtime.LockOSThread and modified any inheritable OS-level +// thread state (for example, Linux or Plan 9 name spaces), the new +// process will inherit the caller's thread state. +// +// StartProcess is a low-level interface. The os/exec package provides +// higher-level interfaces. +// +// If there is an error, it will be of type *PathError. +func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error) { + testlog.Open(name) + return startProcess(name, argv, attr) +} + +// Release releases any resources associated with the Process p, +// rendering it unusable in the future. +// Release only needs to be called if Wait is not. +func (p *Process) Release() error { + return p.release() +} + +// Kill causes the Process to exit immediately. Kill does not wait until +// the Process has actually exited. This only kills the Process itself, +// not any other processes it may have started. +func (p *Process) Kill() error { + return p.kill() +} + +// Wait waits for the Process to exit, and then returns a +// ProcessState describing its status and an error, if any. +// Wait releases any resources associated with the Process. +// On most operating systems, the Process must be a child +// of the current process or an error will be returned. +func (p *Process) Wait() (*ProcessState, error) { + return p.wait() +} + +// Signal sends a signal to the Process. +// Sending Interrupt on Windows is not implemented. +func (p *Process) Signal(sig Signal) error { + return p.signal(sig) +} + +// UserTime returns the user CPU time of the exited process and its children. +func (p *ProcessState) UserTime() time.Duration { + return p.userTime() +} + +// SystemTime returns the system CPU time of the exited process and its children. +func (p *ProcessState) SystemTime() time.Duration { + return p.systemTime() +} + +// Exited reports whether the program has exited. +// On Unix systems this reports true if the program exited due to calling exit, +// but false if the program terminated due to a signal. +func (p *ProcessState) Exited() bool { + return p.exited() +} + +// Success reports whether the program exited successfully, +// such as with exit status 0 on Unix. +func (p *ProcessState) Success() bool { + return p.success() +} + +// Sys returns system-dependent exit information about +// the process. Convert it to the appropriate underlying +// type, such as syscall.WaitStatus on Unix, to access its contents. +func (p *ProcessState) Sys() any { + return p.sys() +} + +// SysUsage returns system-dependent resource usage information about +// the exited process. Convert it to the appropriate underlying +// type, such as *syscall.Rusage on Unix, to access its contents. +// (On Unix, *syscall.Rusage matches struct rusage as defined in the +// getrusage(2) manual page.) +func (p *ProcessState) SysUsage() any { + return p.sysUsage() +} diff --git a/src/os/exec/bench_test.go b/src/os/exec/bench_test.go new file mode 100644 index 0000000..9a94001 --- /dev/null +++ b/src/os/exec/bench_test.go @@ -0,0 +1,23 @@ +// Copyright 2019 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 exec + +import ( + "testing" +) + +func BenchmarkExecHostname(b *testing.B) { + b.ReportAllocs() + path, err := LookPath("hostname") + if err != nil { + b.Fatalf("could not find hostname: %v", err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := Command(path).Run(); err != nil { + b.Fatalf("hostname: %v", err) + } + } +} diff --git a/src/os/exec/dot_test.go b/src/os/exec/dot_test.go new file mode 100644 index 0000000..66c92f7 --- /dev/null +++ b/src/os/exec/dot_test.go @@ -0,0 +1,192 @@ +// Copyright 2022 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 exec_test + +import ( + "errors" + "internal/testenv" + "os" + . "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +var pathVar string = func() string { + if runtime.GOOS == "plan9" { + return "path" + } + return "PATH" +}() + +func TestLookPath(t *testing.T) { + testenv.MustHaveExec(t) + // Not parallel: uses os.Chdir and t.Setenv. + + tmpDir := filepath.Join(t.TempDir(), "testdir") + if err := os.Mkdir(tmpDir, 0777); err != nil { + t.Fatal(err) + } + + executable := "execabs-test" + if runtime.GOOS == "windows" { + executable += ".exe" + } + if err := os.WriteFile(filepath.Join(tmpDir, executable), []byte{1, 2, 3}, 0777); err != nil { + t.Fatal(err) + } + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(cwd); err != nil { + panic(err) + } + }() + if err = os.Chdir(tmpDir); err != nil { + t.Fatal(err) + } + t.Setenv("PWD", tmpDir) + t.Logf(". is %#q", tmpDir) + + origPath := os.Getenv(pathVar) + + // Add "." to PATH so that exec.LookPath looks in the current directory on all systems. + // And try to trick it with "../testdir" too. + for _, errdot := range []string{"1", "0"} { + t.Run("GODEBUG=execerrdot="+errdot, func(t *testing.T) { + t.Setenv("GODEBUG", "execerrdot="+errdot+",execwait=2") + for _, dir := range []string{".", "../testdir"} { + t.Run(pathVar+"="+dir, func(t *testing.T) { + t.Setenv(pathVar, dir+string(filepath.ListSeparator)+origPath) + good := dir + "/execabs-test" + if found, err := LookPath(good); err != nil || !strings.HasPrefix(found, good) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want "%s...", nil`, good, found, err, good) + } + if runtime.GOOS == "windows" { + good = dir + `\execabs-test` + if found, err := LookPath(good); err != nil || !strings.HasPrefix(found, good) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want "%s...", nil`, good, found, err, good) + } + } + + _, err := LookPath("execabs-test") + if errdot == "1" { + if err == nil { + t.Fatalf("LookPath didn't fail when finding a non-relative path") + } else if !errors.Is(err, ErrDot) { + t.Fatalf("LookPath returned unexpected error: want Is ErrDot, got %q", err) + } + } else { + if err != nil { + t.Fatalf("LookPath failed unexpectedly: %v", err) + } + } + + cmd := Command("execabs-test") + if errdot == "1" { + if cmd.Err == nil { + t.Fatalf("Command didn't fail when finding a non-relative path") + } else if !errors.Is(cmd.Err, ErrDot) { + t.Fatalf("Command returned unexpected error: want Is ErrDot, got %q", cmd.Err) + } + cmd.Err = nil + } else { + if cmd.Err != nil { + t.Fatalf("Command failed unexpectedly: %v", err) + } + } + + // Clearing cmd.Err should let the execution proceed, + // and it should fail because it's not a valid binary. + if err := cmd.Run(); err == nil { + t.Fatalf("Run did not fail: expected exec error") + } else if errors.Is(err, ErrDot) { + t.Fatalf("Run returned unexpected error ErrDot: want error like ENOEXEC: %q", err) + } + }) + } + }) + } + + // Test the behavior when the first entry in PATH is an absolute name for the + // current directory. + // + // On Windows, "." may or may not be implicitly included before the explicit + // %PATH%, depending on the process environment; + // see https://go.dev/issue/4394. + // + // If the relative entry from "." resolves to the same executable as what + // would be resolved from an absolute entry in %PATH% alone, LookPath should + // return the absolute version of the path instead of ErrDot. + // (See https://go.dev/issue/53536.) + // + // If PATH does not implicitly include "." (such as on Unix platforms, or on + // Windows configured with NoDefaultCurrentDirectoryInExePath), then this + // lookup should succeed regardless of the behavior for ".", so it may be + // useful to run as a control case even on those platforms. + t.Run(pathVar+"=$PWD", func(t *testing.T) { + t.Setenv(pathVar, tmpDir+string(filepath.ListSeparator)+origPath) + good := filepath.Join(tmpDir, "execabs-test") + if found, err := LookPath(good); err != nil || !strings.HasPrefix(found, good) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want \"%s...\", nil`, good, found, err, good) + } + + if found, err := LookPath("execabs-test"); err != nil || !strings.HasPrefix(found, good) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want \"%s...\", nil`, "execabs-test", found, err, good) + } + + cmd := Command("execabs-test") + if cmd.Err != nil { + t.Fatalf("Command(%#q).Err = %v; want nil", "execabs-test", cmd.Err) + } + }) + + t.Run(pathVar+"=$OTHER", func(t *testing.T) { + // Control case: if the lookup returns ErrDot when PATH is empty, then we + // know that PATH implicitly includes ".". If it does not, then we don't + // expect to see ErrDot at all in this test (because the path will be + // unambiguously absolute). + wantErrDot := false + t.Setenv(pathVar, "") + if found, err := LookPath("execabs-test"); errors.Is(err, ErrDot) { + wantErrDot = true + } else if err == nil { + t.Fatalf(`with PATH='', LookPath(%#q) = %#q; want non-nil error`, "execabs-test", found) + } + + // Set PATH to include an explicit directory that contains a completely + // independent executable that happens to have the same name as an + // executable in ".". If "." is included implicitly, looking up the + // (unqualified) executable name will return ErrDot; otherwise, the + // executable in "." should have no effect and the lookup should + // unambiguously resolve to the directory in PATH. + + dir := t.TempDir() + executable := "execabs-test" + if runtime.GOOS == "windows" { + executable += ".exe" + } + if err := os.WriteFile(filepath.Join(dir, executable), []byte{1, 2, 3}, 0777); err != nil { + t.Fatal(err) + } + t.Setenv(pathVar, dir+string(filepath.ListSeparator)+origPath) + + found, err := LookPath("execabs-test") + if wantErrDot { + wantFound := filepath.Join(".", executable) + if found != wantFound || !errors.Is(err, ErrDot) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want %#q, Is ErrDot`, "execabs-test", found, err, wantFound) + } + } else { + wantFound := filepath.Join(dir, executable) + if found != wantFound || err != nil { + t.Fatalf(`LookPath(%#q) = %#q, %v, want %#q, nil`, "execabs-test", found, err, wantFound) + } + } + }) +} diff --git a/src/os/exec/env_test.go b/src/os/exec/env_test.go new file mode 100644 index 0000000..ea06af3 --- /dev/null +++ b/src/os/exec/env_test.go @@ -0,0 +1,67 @@ +// Copyright 2017 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 exec + +import ( + "reflect" + "testing" +) + +func TestDedupEnv(t *testing.T) { + t.Parallel() + + tests := []struct { + noCase bool + nulOK bool + in []string + want []string + wantErr bool + }{ + { + noCase: true, + in: []string{"k1=v1", "k2=v2", "K1=v3"}, + want: []string{"k2=v2", "K1=v3"}, + }, + { + noCase: false, + in: []string{"k1=v1", "K1=V2", "k1=v3"}, + want: []string{"K1=V2", "k1=v3"}, + }, + { + in: []string{"=a", "=b", "foo", "bar"}, + want: []string{"=b", "foo", "bar"}, + }, + { + // #49886: preserve weird Windows keys with leading "=" signs. + noCase: true, + in: []string{`=C:=C:\golang`, `=D:=D:\tmp`, `=D:=D:\`}, + want: []string{`=C:=C:\golang`, `=D:=D:\`}, + }, + { + // #52436: preserve invalid key-value entries (for now). + // (Maybe filter them out or error out on them at some point.) + in: []string{"dodgy", "entries"}, + want: []string{"dodgy", "entries"}, + }, + { + // Filter out entries containing NULs. + in: []string{"A=a\x00b", "B=b", "C\x00C=c"}, + want: []string{"B=b"}, + wantErr: true, + }, + { + // Plan 9 needs to preserve environment variables with NUL (#56544). + nulOK: true, + in: []string{"path=one\x00two"}, + want: []string{"path=one\x00two"}, + }, + } + for _, tt := range tests { + got, err := dedupEnvCase(tt.noCase, tt.nulOK, tt.in) + if !reflect.DeepEqual(got, tt.want) || (err != nil) != tt.wantErr { + t.Errorf("Dedup(%v, %q) = %q, %v; want %q, error:%v", tt.noCase, tt.in, got, err, tt.want, tt.wantErr) + } + } +} diff --git a/src/os/exec/example_test.go b/src/os/exec/example_test.go new file mode 100644 index 0000000..150f5cf --- /dev/null +++ b/src/os/exec/example_test.go @@ -0,0 +1,169 @@ +// Copyright 2012 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 exec_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "os" + "os/exec" + "strings" + "time" +) + +func ExampleLookPath() { + path, err := exec.LookPath("fortune") + if err != nil { + log.Fatal("installing fortune is in your future") + } + fmt.Printf("fortune is available at %s\n", path) +} + +func ExampleCommand() { + cmd := exec.Command("tr", "a-z", "A-Z") + cmd.Stdin = strings.NewReader("some input") + var out strings.Builder + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + log.Fatal(err) + } + fmt.Printf("in all caps: %q\n", out.String()) +} + +func ExampleCommand_environment() { + cmd := exec.Command("prog") + cmd.Env = append(os.Environ(), + "FOO=duplicate_value", // ignored + "FOO=actual_value", // this value is used + ) + if err := cmd.Run(); err != nil { + log.Fatal(err) + } +} + +func ExampleCmd_Output() { + out, err := exec.Command("date").Output() + if err != nil { + log.Fatal(err) + } + fmt.Printf("The date is %s\n", out) +} + +func ExampleCmd_Run() { + cmd := exec.Command("sleep", "1") + log.Printf("Running command and waiting for it to finish...") + err := cmd.Run() + log.Printf("Command finished with error: %v", err) +} + +func ExampleCmd_Start() { + cmd := exec.Command("sleep", "5") + err := cmd.Start() + if err != nil { + log.Fatal(err) + } + log.Printf("Waiting for command to finish...") + err = cmd.Wait() + log.Printf("Command finished with error: %v", err) +} + +func ExampleCmd_StdoutPipe() { + cmd := exec.Command("echo", "-n", `{"Name": "Bob", "Age": 32}`) + stdout, err := cmd.StdoutPipe() + if err != nil { + log.Fatal(err) + } + if err := cmd.Start(); err != nil { + log.Fatal(err) + } + var person struct { + Name string + Age int + } + if err := json.NewDecoder(stdout).Decode(&person); err != nil { + log.Fatal(err) + } + if err := cmd.Wait(); err != nil { + log.Fatal(err) + } + fmt.Printf("%s is %d years old\n", person.Name, person.Age) +} + +func ExampleCmd_StdinPipe() { + cmd := exec.Command("cat") + stdin, err := cmd.StdinPipe() + if err != nil { + log.Fatal(err) + } + + go func() { + defer stdin.Close() + io.WriteString(stdin, "values written to stdin are passed to cmd's standard input") + }() + + out, err := cmd.CombinedOutput() + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%s\n", out) +} + +func ExampleCmd_StderrPipe() { + cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr") + stderr, err := cmd.StderrPipe() + if err != nil { + log.Fatal(err) + } + + if err := cmd.Start(); err != nil { + log.Fatal(err) + } + + slurp, _ := io.ReadAll(stderr) + fmt.Printf("%s\n", slurp) + + if err := cmd.Wait(); err != nil { + log.Fatal(err) + } +} + +func ExampleCmd_CombinedOutput() { + cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr") + stdoutStderr, err := cmd.CombinedOutput() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", stdoutStderr) +} + +func ExampleCmd_Environ() { + cmd := exec.Command("pwd") + + // Set Dir before calling cmd.Environ so that it will include an + // updated PWD variable (on platforms where that is used). + cmd.Dir = ".." + cmd.Env = append(cmd.Environ(), "POSIXLY_CORRECT=1") + + out, err := cmd.Output() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", out) +} + +func ExampleCommandContext() { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + if err := exec.CommandContext(ctx, "sleep", "5").Run(); err != nil { + // This will fail after 100 milliseconds. The 5 second sleep + // will be interrupted. + } +} diff --git a/src/os/exec/exec.go b/src/os/exec/exec.go new file mode 100644 index 0000000..138be29 --- /dev/null +++ b/src/os/exec/exec.go @@ -0,0 +1,1303 @@ +// Copyright 2009 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 exec runs external commands. It wraps os.StartProcess to make it +// easier to remap stdin and stdout, connect I/O with pipes, and do other +// adjustments. +// +// Unlike the "system" library call from C and other languages, the +// os/exec package intentionally does not invoke the system shell and +// does not expand any glob patterns or handle other expansions, +// pipelines, or redirections typically done by shells. The package +// behaves more like C's "exec" family of functions. To expand glob +// patterns, either call the shell directly, taking care to escape any +// dangerous input, or use the path/filepath package's Glob function. +// To expand environment variables, use package os's ExpandEnv. +// +// Note that the examples in this package assume a Unix system. +// They may not run on Windows, and they do not run in the Go Playground +// used by golang.org and godoc.org. +// +// # Executables in the current directory +// +// The functions Command and LookPath look for a program +// in the directories listed in the current path, following the +// conventions of the host operating system. +// Operating systems have for decades included the current +// directory in this search, sometimes implicitly and sometimes +// configured explicitly that way by default. +// Modern practice is that including the current directory +// is usually unexpected and often leads to security problems. +// +// To avoid those security problems, as of Go 1.19, this package will not resolve a program +// using an implicit or explicit path entry relative to the current directory. +// That is, if you run exec.LookPath("go"), it will not successfully return +// ./go on Unix nor .\go.exe on Windows, no matter how the path is configured. +// Instead, if the usual path algorithms would result in that answer, +// these functions return an error err satisfying errors.Is(err, ErrDot). +// +// For example, consider these two program snippets: +// +// path, err := exec.LookPath("prog") +// if err != nil { +// log.Fatal(err) +// } +// use(path) +// +// and +// +// cmd := exec.Command("prog") +// if err := cmd.Run(); err != nil { +// log.Fatal(err) +// } +// +// These will not find and run ./prog or .\prog.exe, +// no matter how the current path is configured. +// +// Code that always wants to run a program from the current directory +// can be rewritten to say "./prog" instead of "prog". +// +// Code that insists on including results from relative path entries +// can instead override the error using an errors.Is check: +// +// path, err := exec.LookPath("prog") +// if errors.Is(err, exec.ErrDot) { +// err = nil +// } +// if err != nil { +// log.Fatal(err) +// } +// use(path) +// +// and +// +// cmd := exec.Command("prog") +// if errors.Is(cmd.Err, exec.ErrDot) { +// cmd.Err = nil +// } +// if err := cmd.Run(); err != nil { +// log.Fatal(err) +// } +// +// Setting the environment variable GODEBUG=execerrdot=0 +// disables generation of ErrDot entirely, temporarily restoring the pre-Go 1.19 +// behavior for programs that are unable to apply more targeted fixes. +// A future version of Go may remove support for this variable. +// +// Before adding such overrides, make sure you understand the +// security implications of doing so. +// See https://go.dev/blog/path-security for more information. +package exec + +import ( + "bytes" + "context" + "errors" + "internal/godebug" + "internal/syscall/execenv" + "io" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "syscall" + "time" +) + +// Error is returned by LookPath when it fails to classify a file as an +// executable. +type Error struct { + // Name is the file name for which the error occurred. + Name string + // Err is the underlying error. + Err error +} + +func (e *Error) Error() string { + return "exec: " + strconv.Quote(e.Name) + ": " + e.Err.Error() +} + +func (e *Error) Unwrap() error { return e.Err } + +// ErrWaitDelay is returned by (*Cmd).Wait if the process exits with a +// successful status code but its output pipes are not closed before the +// command's WaitDelay expires. +var ErrWaitDelay = errors.New("exec: WaitDelay expired before I/O complete") + +// wrappedError wraps an error without relying on fmt.Errorf. +type wrappedError struct { + prefix string + err error +} + +func (w wrappedError) Error() string { + return w.prefix + ": " + w.err.Error() +} + +func (w wrappedError) Unwrap() error { + return w.err +} + +// Cmd represents an external command being prepared or run. +// +// A Cmd cannot be reused after calling its Run, Output or CombinedOutput +// methods. +type Cmd struct { + // Path is the path of the command to run. + // + // This is the only field that must be set to a non-zero + // value. If Path is relative, it is evaluated relative + // to Dir. + Path string + + // Args holds command line arguments, including the command as Args[0]. + // If the Args field is empty or nil, Run uses {Path}. + // + // In typical use, both Path and Args are set by calling Command. + Args []string + + // Env specifies the environment of the process. + // Each entry is of the form "key=value". + // If Env is nil, the new process uses the current process's + // environment. + // If Env contains duplicate environment keys, only the last + // value in the slice for each duplicate key is used. + // As a special case on Windows, SYSTEMROOT is always added if + // missing and not explicitly set to the empty string. + Env []string + + // Dir specifies the working directory of the command. + // If Dir is the empty string, Run runs the command in the + // calling process's current directory. + Dir string + + // Stdin specifies the process's standard input. + // + // If Stdin is nil, the process reads from the null device (os.DevNull). + // + // If Stdin is an *os.File, the process's standard input is connected + // directly to that file. + // + // Otherwise, during the execution of the command a separate + // goroutine reads from Stdin and delivers that data to the command + // over a pipe. In this case, Wait does not complete until the goroutine + // stops copying, either because it has reached the end of Stdin + // (EOF or a read error), or because writing to the pipe returned an error, + // or because a nonzero WaitDelay was set and expired. + Stdin io.Reader + + // Stdout and Stderr specify the process's standard output and error. + // + // If either is nil, Run connects the corresponding file descriptor + // to the null device (os.DevNull). + // + // If either is an *os.File, the corresponding output from the process + // is connected directly to that file. + // + // Otherwise, during the execution of the command a separate goroutine + // reads from the process over a pipe and delivers that data to the + // corresponding Writer. In this case, Wait does not complete until the + // goroutine reaches EOF or encounters an error or a nonzero WaitDelay + // expires. + // + // If Stdout and Stderr are the same writer, and have a type that can + // be compared with ==, at most one goroutine at a time will call Write. + Stdout io.Writer + Stderr io.Writer + + // ExtraFiles specifies additional open files to be inherited by the + // new process. It does not include standard input, standard output, or + // standard error. If non-nil, entry i becomes file descriptor 3+i. + // + // ExtraFiles is not supported on Windows. + ExtraFiles []*os.File + + // SysProcAttr holds optional, operating system-specific attributes. + // Run passes it to os.StartProcess as the os.ProcAttr's Sys field. + SysProcAttr *syscall.SysProcAttr + + // Process is the underlying process, once started. + Process *os.Process + + // ProcessState contains information about an exited process. + // If the process was started successfully, Wait or Run will + // populate its ProcessState when the command completes. + ProcessState *os.ProcessState + + // ctx is the context passed to CommandContext, if any. + ctx context.Context + + Err error // LookPath error, if any. + + // If Cancel is non-nil, the command must have been created with + // CommandContext and Cancel will be called when the command's + // Context is done. By default, CommandContext sets Cancel to + // call the Kill method on the command's Process. + // + // Typically a custom Cancel will send a signal to the command's + // Process, but it may instead take other actions to initiate cancellation, + // such as closing a stdin or stdout pipe or sending a shutdown request on a + // network socket. + // + // If the command exits with a success status after Cancel is + // called, and Cancel does not return an error equivalent to + // os.ErrProcessDone, then Wait and similar methods will return a non-nil + // error: either an error wrapping the one returned by Cancel, + // or the error from the Context. + // (If the command exits with a non-success status, or Cancel + // returns an error that wraps os.ErrProcessDone, Wait and similar methods + // continue to return the command's usual exit status.) + // + // If Cancel is set to nil, nothing will happen immediately when the command's + // Context is done, but a nonzero WaitDelay will still take effect. That may + // be useful, for example, to work around deadlocks in commands that do not + // support shutdown signals but are expected to always finish quickly. + // + // Cancel will not be called if Start returns a non-nil error. + Cancel func() error + + // If WaitDelay is non-zero, it bounds the time spent waiting on two sources + // of unexpected delay in Wait: a child process that fails to exit after the + // associated Context is canceled, and a child process that exits but leaves + // its I/O pipes unclosed. + // + // The WaitDelay timer starts when either the associated Context is done or a + // call to Wait observes that the child process has exited, whichever occurs + // first. When the delay has elapsed, the command shuts down the child process + // and/or its I/O pipes. + // + // If the child process has failed to exit — perhaps because it ignored or + // failed to receive a shutdown signal from a Cancel function, or because no + // Cancel function was set — then it will be terminated using os.Process.Kill. + // + // Then, if the I/O pipes communicating with the child process are still open, + // those pipes are closed in order to unblock any goroutines currently blocked + // on Read or Write calls. + // + // If pipes are closed due to WaitDelay, no Cancel call has occurred, + // and the command has otherwise exited with a successful status, Wait and + // similar methods will return ErrWaitDelay instead of nil. + // + // If WaitDelay is zero (the default), I/O pipes will be read until EOF, + // which might not occur until orphaned subprocesses of the command have + // also closed their descriptors for the pipes. + WaitDelay time.Duration + + // childIOFiles holds closers for any of the child process's + // stdin, stdout, and/or stderr files that were opened by the Cmd itself + // (not supplied by the caller). These should be closed as soon as they + // are inherited by the child process. + childIOFiles []io.Closer + + // parentIOPipes holds closers for the parent's end of any pipes + // connected to the child's stdin, stdout, and/or stderr streams + // that were opened by the Cmd itself (not supplied by the caller). + // These should be closed after Wait sees the command and copying + // goroutines exit, or after WaitDelay has expired. + parentIOPipes []io.Closer + + // goroutine holds a set of closures to execute to copy data + // to and/or from the command's I/O pipes. + goroutine []func() error + + // If goroutineErr is non-nil, it receives the first error from a copying + // goroutine once all such goroutines have completed. + // goroutineErr is set to nil once its error has been received. + goroutineErr <-chan error + + // If ctxResult is non-nil, it receives the result of watchCtx exactly once. + ctxResult <-chan ctxResult + + // The stack saved when the Command was created, if GODEBUG contains + // execwait=2. Used for debugging leaks. + createdByStack []byte + + // For a security release long ago, we created x/sys/execabs, + // which manipulated the unexported lookPathErr error field + // in this struct. For Go 1.19 we exported the field as Err error, + // above, but we have to keep lookPathErr around for use by + // old programs building against new toolchains. + // The String and Start methods look for an error in lookPathErr + // in preference to Err, to preserve the errors that execabs sets. + // + // In general we don't guarantee misuse of reflect like this, + // but the misuse of reflect was by us, the best of various bad + // options to fix the security problem, and people depend on + // those old copies of execabs continuing to work. + // The result is that we have to leave this variable around for the + // rest of time, a compatibility scar. + // + // See https://go.dev/blog/path-security + // and https://go.dev/issue/43724 for more context. + lookPathErr error +} + +// A ctxResult reports the result of watching the Context associated with a +// running command (and sending corresponding signals if needed). +type ctxResult struct { + err error + + // If timer is non-nil, it expires after WaitDelay has elapsed after + // the Context is done. + // + // (If timer is nil, that means that the Context was not done before the + // command completed, or no WaitDelay was set, or the WaitDelay already + // expired and its effect was already applied.) + timer *time.Timer +} + +var execwait = godebug.New("#execwait") +var execerrdot = godebug.New("execerrdot") + +// Command returns the Cmd struct to execute the named program with +// the given arguments. +// +// It sets only the Path and Args in the returned structure. +// +// If name contains no path separators, Command uses LookPath to +// resolve name to a complete path if possible. Otherwise it uses name +// directly as Path. +// +// The returned Cmd's Args field is constructed from the command name +// followed by the elements of arg, so arg should not include the +// command name itself. For example, Command("echo", "hello"). +// Args[0] is always name, not the possibly resolved Path. +// +// On Windows, processes receive the whole command line as a single string +// and do their own parsing. Command combines and quotes Args into a command +// line string with an algorithm compatible with applications using +// CommandLineToArgvW (which is the most common way). Notable exceptions are +// msiexec.exe and cmd.exe (and thus, all batch files), which have a different +// unquoting algorithm. In these or other similar cases, you can do the +// quoting yourself and provide the full command line in SysProcAttr.CmdLine, +// leaving Args empty. +func Command(name string, arg ...string) *Cmd { + cmd := &Cmd{ + Path: name, + Args: append([]string{name}, arg...), + } + + if v := execwait.Value(); v != "" { + if v == "2" { + // Obtain the caller stack. (This is equivalent to runtime/debug.Stack, + // copied to avoid importing the whole package.) + stack := make([]byte, 1024) + for { + n := runtime.Stack(stack, false) + if n < len(stack) { + stack = stack[:n] + break + } + stack = make([]byte, 2*len(stack)) + } + + if i := bytes.Index(stack, []byte("\nos/exec.Command(")); i >= 0 { + stack = stack[i+1:] + } + cmd.createdByStack = stack + } + + runtime.SetFinalizer(cmd, func(c *Cmd) { + if c.Process != nil && c.ProcessState == nil { + debugHint := "" + if c.createdByStack == nil { + debugHint = " (set GODEBUG=execwait=2 to capture stacks for debugging)" + } else { + os.Stderr.WriteString("GODEBUG=execwait=2 detected a leaked exec.Cmd created by:\n") + os.Stderr.Write(c.createdByStack) + os.Stderr.WriteString("\n") + debugHint = "" + } + panic("exec: Cmd started a Process but leaked without a call to Wait" + debugHint) + } + }) + } + + if filepath.Base(name) == name { + lp, err := LookPath(name) + if lp != "" { + // Update cmd.Path even if err is non-nil. + // If err is ErrDot (especially on Windows), lp may include a resolved + // extension (like .exe or .bat) that should be preserved. + cmd.Path = lp + } + if err != nil { + cmd.Err = err + } + } + return cmd +} + +// CommandContext is like Command but includes a context. +// +// The provided context is used to interrupt the process +// (by calling cmd.Cancel or os.Process.Kill) +// if the context becomes done before the command completes on its own. +// +// CommandContext sets the command's Cancel function to invoke the Kill method +// on its Process, and leaves its WaitDelay unset. The caller may change the +// cancellation behavior by modifying those fields before starting the command. +func CommandContext(ctx context.Context, name string, arg ...string) *Cmd { + if ctx == nil { + panic("nil Context") + } + cmd := Command(name, arg...) + cmd.ctx = ctx + cmd.Cancel = func() error { + return cmd.Process.Kill() + } + return cmd +} + +// String returns a human-readable description of c. +// It is intended only for debugging. +// In particular, it is not suitable for use as input to a shell. +// The output of String may vary across Go releases. +func (c *Cmd) String() string { + if c.Err != nil || c.lookPathErr != nil { + // failed to resolve path; report the original requested path (plus args) + return strings.Join(c.Args, " ") + } + // report the exact executable path (plus args) + b := new(strings.Builder) + b.WriteString(c.Path) + for _, a := range c.Args[1:] { + b.WriteByte(' ') + b.WriteString(a) + } + return b.String() +} + +// interfaceEqual protects against panics from doing equality tests on +// two interfaces with non-comparable underlying types. +func interfaceEqual(a, b any) bool { + defer func() { + recover() + }() + return a == b +} + +func (c *Cmd) argv() []string { + if len(c.Args) > 0 { + return c.Args + } + return []string{c.Path} +} + +func (c *Cmd) childStdin() (*os.File, error) { + if c.Stdin == nil { + f, err := os.Open(os.DevNull) + if err != nil { + return nil, err + } + c.childIOFiles = append(c.childIOFiles, f) + return f, nil + } + + if f, ok := c.Stdin.(*os.File); ok { + return f, nil + } + + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + + c.childIOFiles = append(c.childIOFiles, pr) + c.parentIOPipes = append(c.parentIOPipes, pw) + c.goroutine = append(c.goroutine, func() error { + _, err := io.Copy(pw, c.Stdin) + if skipStdinCopyError(err) { + err = nil + } + if err1 := pw.Close(); err == nil { + err = err1 + } + return err + }) + return pr, nil +} + +func (c *Cmd) childStdout() (*os.File, error) { + return c.writerDescriptor(c.Stdout) +} + +func (c *Cmd) childStderr(childStdout *os.File) (*os.File, error) { + if c.Stderr != nil && interfaceEqual(c.Stderr, c.Stdout) { + return childStdout, nil + } + return c.writerDescriptor(c.Stderr) +} + +// writerDescriptor returns an os.File to which the child process +// can write to send data to w. +// +// If w is nil, writerDescriptor returns a File that writes to os.DevNull. +func (c *Cmd) writerDescriptor(w io.Writer) (*os.File, error) { + if w == nil { + f, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0) + if err != nil { + return nil, err + } + c.childIOFiles = append(c.childIOFiles, f) + return f, nil + } + + if f, ok := w.(*os.File); ok { + return f, nil + } + + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + + c.childIOFiles = append(c.childIOFiles, pw) + c.parentIOPipes = append(c.parentIOPipes, pr) + c.goroutine = append(c.goroutine, func() error { + _, err := io.Copy(w, pr) + pr.Close() // in case io.Copy stopped due to write error + return err + }) + return pw, nil +} + +func closeDescriptors(closers []io.Closer) { + for _, fd := range closers { + fd.Close() + } +} + +// Run starts the specified command and waits for it to complete. +// +// The returned error is nil if the command runs, has no problems +// copying stdin, stdout, and stderr, and exits with a zero exit +// status. +// +// If the command starts but does not complete successfully, the error is of +// type *ExitError. Other error types may be returned for other situations. +// +// If the calling goroutine has locked the operating system thread +// with runtime.LockOSThread and modified any inheritable OS-level +// thread state (for example, Linux or Plan 9 name spaces), the new +// process will inherit the caller's thread state. +func (c *Cmd) Run() error { + if err := c.Start(); err != nil { + return err + } + return c.Wait() +} + +// lookExtensions finds windows executable by its dir and path. +// It uses LookPath to try appropriate extensions. +// lookExtensions does not search PATH, instead it converts `prog` into `.\prog`. +func lookExtensions(path, dir string) (string, error) { + if filepath.Base(path) == path { + path = "." + string(filepath.Separator) + path + } + if dir == "" { + return LookPath(path) + } + if filepath.VolumeName(path) != "" { + return LookPath(path) + } + if len(path) > 1 && os.IsPathSeparator(path[0]) { + return LookPath(path) + } + dirandpath := filepath.Join(dir, path) + // We assume that LookPath will only add file extension. + lp, err := LookPath(dirandpath) + if err != nil { + return "", err + } + ext := strings.TrimPrefix(lp, dirandpath) + return path + ext, nil +} + +// Start starts the specified command but does not wait for it to complete. +// +// If Start returns successfully, the c.Process field will be set. +// +// After a successful call to Start the Wait method must be called in +// order to release associated system resources. +func (c *Cmd) Start() error { + // Check for doubled Start calls before we defer failure cleanup. If the prior + // call to Start succeeded, we don't want to spuriously close its pipes. + if c.Process != nil { + return errors.New("exec: already started") + } + + started := false + defer func() { + closeDescriptors(c.childIOFiles) + c.childIOFiles = nil + + if !started { + closeDescriptors(c.parentIOPipes) + c.parentIOPipes = nil + } + }() + + if c.Path == "" && c.Err == nil && c.lookPathErr == nil { + c.Err = errors.New("exec: no command") + } + if c.Err != nil || c.lookPathErr != nil { + if c.lookPathErr != nil { + return c.lookPathErr + } + return c.Err + } + if runtime.GOOS == "windows" { + lp, err := lookExtensions(c.Path, c.Dir) + if err != nil { + return err + } + c.Path = lp + } + if c.Cancel != nil && c.ctx == nil { + return errors.New("exec: command with a non-nil Cancel was not created with CommandContext") + } + if c.ctx != nil { + select { + case <-c.ctx.Done(): + return c.ctx.Err() + default: + } + } + + childFiles := make([]*os.File, 0, 3+len(c.ExtraFiles)) + stdin, err := c.childStdin() + if err != nil { + return err + } + childFiles = append(childFiles, stdin) + stdout, err := c.childStdout() + if err != nil { + return err + } + childFiles = append(childFiles, stdout) + stderr, err := c.childStderr(stdout) + if err != nil { + return err + } + childFiles = append(childFiles, stderr) + childFiles = append(childFiles, c.ExtraFiles...) + + env, err := c.environ() + if err != nil { + return err + } + + c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{ + Dir: c.Dir, + Files: childFiles, + Env: env, + Sys: c.SysProcAttr, + }) + if err != nil { + return err + } + started = true + + // Don't allocate the goroutineErr channel unless there are goroutines to start. + if len(c.goroutine) > 0 { + goroutineErr := make(chan error, 1) + c.goroutineErr = goroutineErr + + type goroutineStatus struct { + running int + firstErr error + } + statusc := make(chan goroutineStatus, 1) + statusc <- goroutineStatus{running: len(c.goroutine)} + for _, fn := range c.goroutine { + go func(fn func() error) { + err := fn() + + status := <-statusc + if status.firstErr == nil { + status.firstErr = err + } + status.running-- + if status.running == 0 { + goroutineErr <- status.firstErr + } else { + statusc <- status + } + }(fn) + } + c.goroutine = nil // Allow the goroutines' closures to be GC'd when they complete. + } + + // If we have anything to do when the command's Context expires, + // start a goroutine to watch for cancellation. + // + // (Even if the command was created by CommandContext, a helper library may + // have explicitly set its Cancel field back to nil, indicating that it should + // be allowed to continue running after cancellation after all.) + if (c.Cancel != nil || c.WaitDelay != 0) && c.ctx != nil && c.ctx.Done() != nil { + resultc := make(chan ctxResult) + c.ctxResult = resultc + go c.watchCtx(resultc) + } + + return nil +} + +// watchCtx watches c.ctx until it is able to send a result to resultc. +// +// If c.ctx is done before a result can be sent, watchCtx calls c.Cancel, +// and/or kills cmd.Process it after c.WaitDelay has elapsed. +// +// watchCtx manipulates c.goroutineErr, so its result must be received before +// c.awaitGoroutines is called. +func (c *Cmd) watchCtx(resultc chan<- ctxResult) { + select { + case resultc <- ctxResult{}: + return + case <-c.ctx.Done(): + } + + var err error + if c.Cancel != nil { + if interruptErr := c.Cancel(); interruptErr == nil { + // We appear to have successfully interrupted the command, so any + // program behavior from this point may be due to ctx even if the + // command exits with code 0. + err = c.ctx.Err() + } else if errors.Is(interruptErr, os.ErrProcessDone) { + // The process already finished: we just didn't notice it yet. + // (Perhaps c.Wait hadn't been called, or perhaps it happened to race with + // c.ctx being cancelled.) Don't inject a needless error. + } else { + err = wrappedError{ + prefix: "exec: canceling Cmd", + err: interruptErr, + } + } + } + if c.WaitDelay == 0 { + resultc <- ctxResult{err: err} + return + } + + timer := time.NewTimer(c.WaitDelay) + select { + case resultc <- ctxResult{err: err, timer: timer}: + // c.Process.Wait returned and we've handed the timer off to c.Wait. + // It will take care of goroutine shutdown from here. + return + case <-timer.C: + } + + killed := false + if killErr := c.Process.Kill(); killErr == nil { + // We appear to have killed the process. c.Process.Wait should return a + // non-nil error to c.Wait unless the Kill signal races with a successful + // exit, and if that does happen we shouldn't report a spurious error, + // so don't set err to anything here. + killed = true + } else if !errors.Is(killErr, os.ErrProcessDone) { + err = wrappedError{ + prefix: "exec: killing Cmd", + err: killErr, + } + } + + if c.goroutineErr != nil { + select { + case goroutineErr := <-c.goroutineErr: + // Forward goroutineErr only if we don't have reason to believe it was + // caused by a call to Cancel or Kill above. + if err == nil && !killed { + err = goroutineErr + } + default: + // Close the child process's I/O pipes, in case it abandoned some + // subprocess that inherited them and is still holding them open + // (see https://go.dev/issue/23019). + // + // We close the goroutine pipes only after we have sent any signals we're + // going to send to the process (via Signal or Kill above): if we send + // SIGKILL to the process, we would prefer for it to die of SIGKILL, not + // SIGPIPE. (However, this may still cause any orphaned subprocesses to + // terminate with SIGPIPE.) + closeDescriptors(c.parentIOPipes) + // Wait for the copying goroutines to finish, but report ErrWaitDelay for + // the error: any other error here could result from closing the pipes. + _ = <-c.goroutineErr + if err == nil { + err = ErrWaitDelay + } + } + + // Since we have already received the only result from c.goroutineErr, + // set it to nil to prevent awaitGoroutines from blocking on it. + c.goroutineErr = nil + } + + resultc <- ctxResult{err: err} +} + +// An ExitError reports an unsuccessful exit by a command. +type ExitError struct { + *os.ProcessState + + // Stderr holds a subset of the standard error output from the + // Cmd.Output method if standard error was not otherwise being + // collected. + // + // If the error output is long, Stderr may contain only a prefix + // and suffix of the output, with the middle replaced with + // text about the number of omitted bytes. + // + // Stderr is provided for debugging, for inclusion in error messages. + // Users with other needs should redirect Cmd.Stderr as needed. + Stderr []byte +} + +func (e *ExitError) Error() string { + return e.ProcessState.String() +} + +// Wait waits for the command to exit and waits for any copying to +// stdin or copying from stdout or stderr to complete. +// +// The command must have been started by Start. +// +// The returned error is nil if the command runs, has no problems +// copying stdin, stdout, and stderr, and exits with a zero exit +// status. +// +// If the command fails to run or doesn't complete successfully, the +// error is of type *ExitError. Other error types may be +// returned for I/O problems. +// +// If any of c.Stdin, c.Stdout or c.Stderr are not an *os.File, Wait also waits +// for the respective I/O loop copying to or from the process to complete. +// +// Wait releases any resources associated with the Cmd. +func (c *Cmd) Wait() error { + if c.Process == nil { + return errors.New("exec: not started") + } + if c.ProcessState != nil { + return errors.New("exec: Wait was already called") + } + + state, err := c.Process.Wait() + if err == nil && !state.Success() { + err = &ExitError{ProcessState: state} + } + c.ProcessState = state + + var timer *time.Timer + if c.ctxResult != nil { + watch := <-c.ctxResult + timer = watch.timer + // If c.Process.Wait returned an error, prefer that. + // Otherwise, report any error from the watchCtx goroutine, + // such as a Context cancellation or a WaitDelay overrun. + if err == nil && watch.err != nil { + err = watch.err + } + } + + if goroutineErr := c.awaitGoroutines(timer); err == nil { + // Report an error from the copying goroutines only if the program otherwise + // exited normally on its own. Otherwise, the copying error may be due to the + // abnormal termination. + err = goroutineErr + } + closeDescriptors(c.parentIOPipes) + c.parentIOPipes = nil + + return err +} + +// awaitGoroutines waits for the results of the goroutines copying data to or +// from the command's I/O pipes. +// +// If c.WaitDelay elapses before the goroutines complete, awaitGoroutines +// forcibly closes their pipes and returns ErrWaitDelay. +// +// If timer is non-nil, it must send to timer.C at the end of c.WaitDelay. +func (c *Cmd) awaitGoroutines(timer *time.Timer) error { + defer func() { + if timer != nil { + timer.Stop() + } + c.goroutineErr = nil + }() + + if c.goroutineErr == nil { + return nil // No running goroutines to await. + } + + if timer == nil { + if c.WaitDelay == 0 { + return <-c.goroutineErr + } + + select { + case err := <-c.goroutineErr: + // Avoid the overhead of starting a timer. + return err + default: + } + + // No existing timer was started: either there is no Context associated with + // the command, or c.Process.Wait completed before the Context was done. + timer = time.NewTimer(c.WaitDelay) + } + + select { + case <-timer.C: + closeDescriptors(c.parentIOPipes) + // Wait for the copying goroutines to finish, but ignore any error + // (since it was probably caused by closing the pipes). + _ = <-c.goroutineErr + return ErrWaitDelay + + case err := <-c.goroutineErr: + return err + } +} + +// Output runs the command and returns its standard output. +// Any returned error will usually be of type *ExitError. +// If c.Stderr was nil, Output populates ExitError.Stderr. +func (c *Cmd) Output() ([]byte, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + var stdout bytes.Buffer + c.Stdout = &stdout + + captureErr := c.Stderr == nil + if captureErr { + c.Stderr = &prefixSuffixSaver{N: 32 << 10} + } + + err := c.Run() + if err != nil && captureErr { + if ee, ok := err.(*ExitError); ok { + ee.Stderr = c.Stderr.(*prefixSuffixSaver).Bytes() + } + } + return stdout.Bytes(), err +} + +// CombinedOutput runs the command and returns its combined standard +// output and standard error. +func (c *Cmd) CombinedOutput() ([]byte, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + if c.Stderr != nil { + return nil, errors.New("exec: Stderr already set") + } + var b bytes.Buffer + c.Stdout = &b + c.Stderr = &b + err := c.Run() + return b.Bytes(), err +} + +// StdinPipe returns a pipe that will be connected to the command's +// standard input when the command starts. +// The pipe will be closed automatically after Wait sees the command exit. +// A caller need only call Close to force the pipe to close sooner. +// For example, if the command being run will not exit until standard input +// is closed, the caller must close the pipe. +func (c *Cmd) StdinPipe() (io.WriteCloser, error) { + if c.Stdin != nil { + return nil, errors.New("exec: Stdin already set") + } + if c.Process != nil { + return nil, errors.New("exec: StdinPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stdin = pr + c.childIOFiles = append(c.childIOFiles, pr) + c.parentIOPipes = append(c.parentIOPipes, pw) + return pw, nil +} + +// StdoutPipe returns a pipe that will be connected to the command's +// standard output when the command starts. +// +// Wait will close the pipe after seeing the command exit, so most callers +// need not close the pipe themselves. It is thus incorrect to call Wait +// before all reads from the pipe have completed. +// For the same reason, it is incorrect to call Run when using StdoutPipe. +// See the example for idiomatic usage. +func (c *Cmd) StdoutPipe() (io.ReadCloser, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + if c.Process != nil { + return nil, errors.New("exec: StdoutPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stdout = pw + c.childIOFiles = append(c.childIOFiles, pw) + c.parentIOPipes = append(c.parentIOPipes, pr) + return pr, nil +} + +// StderrPipe returns a pipe that will be connected to the command's +// standard error when the command starts. +// +// Wait will close the pipe after seeing the command exit, so most callers +// need not close the pipe themselves. It is thus incorrect to call Wait +// before all reads from the pipe have completed. +// For the same reason, it is incorrect to use Run when using StderrPipe. +// See the StdoutPipe example for idiomatic usage. +func (c *Cmd) StderrPipe() (io.ReadCloser, error) { + if c.Stderr != nil { + return nil, errors.New("exec: Stderr already set") + } + if c.Process != nil { + return nil, errors.New("exec: StderrPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stderr = pw + c.childIOFiles = append(c.childIOFiles, pw) + c.parentIOPipes = append(c.parentIOPipes, pr) + return pr, nil +} + +// prefixSuffixSaver is an io.Writer which retains the first N bytes +// and the last N bytes written to it. The Bytes() methods reconstructs +// it with a pretty error message. +type prefixSuffixSaver struct { + N int // max size of prefix or suffix + prefix []byte + suffix []byte // ring buffer once len(suffix) == N + suffixOff int // offset to write into suffix + skipped int64 + + // TODO(bradfitz): we could keep one large []byte and use part of it for + // the prefix, reserve space for the '... Omitting N bytes ...' message, + // then the ring buffer suffix, and just rearrange the ring buffer + // suffix when Bytes() is called, but it doesn't seem worth it for + // now just for error messages. It's only ~64KB anyway. +} + +func (w *prefixSuffixSaver) Write(p []byte) (n int, err error) { + lenp := len(p) + p = w.fill(&w.prefix, p) + + // Only keep the last w.N bytes of suffix data. + if overage := len(p) - w.N; overage > 0 { + p = p[overage:] + w.skipped += int64(overage) + } + p = w.fill(&w.suffix, p) + + // w.suffix is full now if p is non-empty. Overwrite it in a circle. + for len(p) > 0 { // 0, 1, or 2 iterations. + n := copy(w.suffix[w.suffixOff:], p) + p = p[n:] + w.skipped += int64(n) + w.suffixOff += n + if w.suffixOff == w.N { + w.suffixOff = 0 + } + } + return lenp, nil +} + +// fill appends up to len(p) bytes of p to *dst, such that *dst does not +// grow larger than w.N. It returns the un-appended suffix of p. +func (w *prefixSuffixSaver) fill(dst *[]byte, p []byte) (pRemain []byte) { + if remain := w.N - len(*dst); remain > 0 { + add := minInt(len(p), remain) + *dst = append(*dst, p[:add]...) + p = p[add:] + } + return p +} + +func (w *prefixSuffixSaver) Bytes() []byte { + if w.suffix == nil { + return w.prefix + } + if w.skipped == 0 { + return append(w.prefix, w.suffix...) + } + var buf bytes.Buffer + buf.Grow(len(w.prefix) + len(w.suffix) + 50) + buf.Write(w.prefix) + buf.WriteString("\n... omitting ") + buf.WriteString(strconv.FormatInt(w.skipped, 10)) + buf.WriteString(" bytes ...\n") + buf.Write(w.suffix[w.suffixOff:]) + buf.Write(w.suffix[:w.suffixOff]) + return buf.Bytes() +} + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +// environ returns a best-effort copy of the environment in which the command +// would be run as it is currently configured. If an error occurs in computing +// the environment, it is returned alongside the best-effort copy. +func (c *Cmd) environ() ([]string, error) { + var err error + + env := c.Env + if env == nil { + env, err = execenv.Default(c.SysProcAttr) + if err != nil { + env = os.Environ() + // Note that the non-nil err is preserved despite env being overridden. + } + + if c.Dir != "" { + switch runtime.GOOS { + case "windows", "plan9": + // Windows and Plan 9 do not use the PWD variable, so we don't need to + // keep it accurate. + default: + // On POSIX platforms, PWD represents “an absolute pathname of the + // current working directory.” Since we are changing the working + // directory for the command, we should also update PWD to reflect that. + // + // Unfortunately, we didn't always do that, so (as proposed in + // https://go.dev/issue/50599) to avoid unintended collateral damage we + // only implicitly update PWD when Env is nil. That way, we're much + // less likely to override an intentional change to the variable. + if pwd, absErr := filepath.Abs(c.Dir); absErr == nil { + env = append(env, "PWD="+pwd) + } else if err == nil { + err = absErr + } + } + } + } + + env, dedupErr := dedupEnv(env) + if err == nil { + err = dedupErr + } + return addCriticalEnv(env), err +} + +// Environ returns a copy of the environment in which the command would be run +// as it is currently configured. +func (c *Cmd) Environ() []string { + // Intentionally ignore errors: environ returns a best-effort environment no matter what. + env, _ := c.environ() + return env +} + +// dedupEnv returns a copy of env with any duplicates removed, in favor of +// later values. +// Items not of the normal environment "key=value" form are preserved unchanged. +// Except on Plan 9, items containing NUL characters are removed, and +// an error is returned along with the remaining values. +func dedupEnv(env []string) ([]string, error) { + return dedupEnvCase(runtime.GOOS == "windows", runtime.GOOS == "plan9", env) +} + +// dedupEnvCase is dedupEnv with a case option for testing. +// If caseInsensitive is true, the case of keys is ignored. +// If nulOK is false, items containing NUL characters are allowed. +func dedupEnvCase(caseInsensitive, nulOK bool, env []string) ([]string, error) { + // Construct the output in reverse order, to preserve the + // last occurrence of each key. + var err error + out := make([]string, 0, len(env)) + saw := make(map[string]bool, len(env)) + for n := len(env); n > 0; n-- { + kv := env[n-1] + + // Reject NUL in environment variables to prevent security issues (#56284); + // except on Plan 9, which uses NUL as os.PathListSeparator (#56544). + if !nulOK && strings.IndexByte(kv, 0) != -1 { + err = errors.New("exec: environment variable contains NUL") + continue + } + + i := strings.Index(kv, "=") + if i == 0 { + // We observe in practice keys with a single leading "=" on Windows. + // TODO(#49886): Should we consume only the first leading "=" as part + // of the key, or parse through arbitrarily many of them until a non-"="? + i = strings.Index(kv[1:], "=") + 1 + } + if i < 0 { + if kv != "" { + // The entry is not of the form "key=value" (as it is required to be). + // Leave it as-is for now. + // TODO(#52436): should we strip or reject these bogus entries? + out = append(out, kv) + } + continue + } + k := kv[:i] + if caseInsensitive { + k = strings.ToLower(k) + } + if saw[k] { + continue + } + + saw[k] = true + out = append(out, kv) + } + + // Now reverse the slice to restore the original order. + for i := 0; i < len(out)/2; i++ { + j := len(out) - i - 1 + out[i], out[j] = out[j], out[i] + } + + return out, err +} + +// addCriticalEnv adds any critical environment variables that are required +// (or at least almost always required) on the operating system. +// Currently this is only used for Windows. +func addCriticalEnv(env []string) []string { + if runtime.GOOS != "windows" { + return env + } + for _, kv := range env { + k, _, ok := strings.Cut(kv, "=") + if !ok { + continue + } + if strings.EqualFold(k, "SYSTEMROOT") { + // We already have it. + return env + } + } + return append(env, "SYSTEMROOT="+os.Getenv("SYSTEMROOT")) +} + +// ErrDot indicates that a path lookup resolved to an executable +// in the current directory due to ‘.’ being in the path, either +// implicitly or explicitly. See the package documentation for details. +// +// Note that functions in this package do not return ErrDot directly. +// Code should use errors.Is(err, ErrDot), not err == ErrDot, +// to test whether a returned error err is due to this condition. +var ErrDot = errors.New("cannot run executable found relative to current directory") diff --git a/src/os/exec/exec_linux_test.go b/src/os/exec/exec_linux_test.go new file mode 100644 index 0000000..b9f6b7b --- /dev/null +++ b/src/os/exec/exec_linux_test.go @@ -0,0 +1,45 @@ +// 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. + +//go:build linux && cgo + +// On systems that use glibc, calling malloc can create a new arena, +// and creating a new arena can read /sys/devices/system/cpu/online. +// If we are using cgo, we will call malloc when creating a new thread. +// That can break TestExtraFiles if we create a new thread that creates +// a new arena and opens the /sys file while we are checking for open +// file descriptors. Work around the problem by creating threads up front. +// See issue 25628. + +package exec_test + +import ( + "os" + "sync" + "syscall" + "time" +) + +func init() { + if os.Getenv("GO_EXEC_TEST_PID") == "" { + return + } + + // Start some threads. 10 is arbitrary but intended to be enough + // to ensure that the code won't have to create any threads itself. + // In particular this should be more than the number of threads + // the garbage collector might create. + const threads = 10 + + var wg sync.WaitGroup + wg.Add(threads) + ts := syscall.NsecToTimespec((100 * time.Microsecond).Nanoseconds()) + for i := 0; i < threads; i++ { + go func() { + defer wg.Done() + syscall.Nanosleep(&ts, nil) + }() + } + wg.Wait() +} diff --git a/src/os/exec/exec_other_test.go b/src/os/exec/exec_other_test.go new file mode 100644 index 0000000..64c819c --- /dev/null +++ b/src/os/exec/exec_other_test.go @@ -0,0 +1,14 @@ +// Copyright 2021 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. + +//go:build !unix && !windows + +package exec_test + +import "os" + +var ( + quitSignal os.Signal = nil + pipeSignal os.Signal = nil +) diff --git a/src/os/exec/exec_plan9.go b/src/os/exec/exec_plan9.go new file mode 100644 index 0000000..8920bec --- /dev/null +++ b/src/os/exec/exec_plan9.go @@ -0,0 +1,19 @@ +// Copyright 2019 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 exec + +import "io/fs" + +// skipStdinCopyError optionally specifies a function which reports +// whether the provided stdin copy error should be ignored. +func skipStdinCopyError(err error) bool { + // Ignore hungup errors copying to stdin if the program + // completed successfully otherwise. + // See Issue 35753. + pe, ok := err.(*fs.PathError) + return ok && + pe.Op == "write" && pe.Path == "|1" && + pe.Err.Error() == "i/o on hungup channel" +} diff --git a/src/os/exec/exec_posix_test.go b/src/os/exec/exec_posix_test.go new file mode 100644 index 0000000..5d828b3 --- /dev/null +++ b/src/os/exec/exec_posix_test.go @@ -0,0 +1,276 @@ +// Copyright 2017 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. + +//go:build unix + +package exec_test + +import ( + "fmt" + "internal/testenv" + "io" + "os" + "os/user" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + "syscall" + "testing" + "time" +) + +func init() { + registerHelperCommand("pwd", cmdPwd) +} + +func cmdPwd(...string) { + pwd, err := os.Getwd() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Println(pwd) +} + +func TestCredentialNoSetGroups(t *testing.T) { + if runtime.GOOS == "android" { + maySkipHelperCommand("echo") + t.Skip("unsupported on Android") + } + t.Parallel() + + u, err := user.Current() + if err != nil { + t.Fatalf("error getting current user: %v", err) + } + + uid, err := strconv.Atoi(u.Uid) + if err != nil { + t.Fatalf("error converting Uid=%s to integer: %v", u.Uid, err) + } + + gid, err := strconv.Atoi(u.Gid) + if err != nil { + t.Fatalf("error converting Gid=%s to integer: %v", u.Gid, err) + } + + // If NoSetGroups is true, setgroups isn't called and cmd.Run should succeed + cmd := helperCommand(t, "echo", "foo") + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(gid), + NoSetGroups: true, + }, + } + + if err = cmd.Run(); err != nil { + t.Errorf("Failed to run command: %v", err) + } +} + +// For issue #19314: make sure that SIGSTOP does not cause the process +// to appear done. +func TestWaitid(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + // Wait for the child process to come up and register any signal handlers. + const msg = "O:ping\n" + if _, err := io.WriteString(stdin, msg); err != nil { + t.Fatal(err) + } + buf := make([]byte, len(msg)) + if _, err := io.ReadFull(stdout, buf); err != nil { + t.Fatal(err) + } + // Now leave the pipes open so that the process will hang until we close stdin. + + if err := cmd.Process.Signal(syscall.SIGSTOP); err != nil { + cmd.Process.Kill() + t.Fatal(err) + } + + ch := make(chan error) + go func() { + ch <- cmd.Wait() + }() + + // Give a little time for Wait to block on waiting for the process. + // (This is just to give some time to trigger the bug; it should not be + // necessary for the test to pass.) + if testing.Short() { + time.Sleep(1 * time.Millisecond) + } else { + time.Sleep(10 * time.Millisecond) + } + + // This call to Signal should succeed because the process still exists. + // (Prior to the fix for #19314, this would fail with os.ErrProcessDone + // or an equivalent error.) + if err := cmd.Process.Signal(syscall.SIGCONT); err != nil { + t.Error(err) + syscall.Kill(cmd.Process.Pid, syscall.SIGCONT) + } + + // The SIGCONT should allow the process to wake up, notice that stdin + // is closed, and exit successfully. + stdin.Close() + err = <-ch + if err != nil { + t.Fatal(err) + } +} + +// https://go.dev/issue/50599: if Env is not set explicitly, setting Dir should +// implicitly update PWD to the correct path, and Environ should list the +// updated value. +func TestImplicitPWD(t *testing.T) { + t.Parallel() + + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + cases := []struct { + name string + dir string + want string + }{ + {"empty", "", cwd}, + {"dot", ".", cwd}, + {"dotdot", "..", filepath.Dir(cwd)}, + {"PWD", cwd, cwd}, + {"PWDdotdot", cwd + string(filepath.Separator) + "..", filepath.Dir(cwd)}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pwd") + if cmd.Env != nil { + t.Fatalf("test requires helperCommand not to set Env field") + } + cmd.Dir = tc.dir + + var pwds []string + for _, kv := range cmd.Environ() { + if strings.HasPrefix(kv, "PWD=") { + pwds = append(pwds, strings.TrimPrefix(kv, "PWD=")) + } + } + + wantPWDs := []string{tc.want} + if tc.dir == "" { + if _, ok := os.LookupEnv("PWD"); !ok { + wantPWDs = nil + } + } + if !reflect.DeepEqual(pwds, wantPWDs) { + t.Errorf("PWD entries in cmd.Environ():\n\t%s\nwant:\n\t%s", strings.Join(pwds, "\n\t"), strings.Join(wantPWDs, "\n\t")) + } + + cmd.Stderr = new(strings.Builder) + out, err := cmd.Output() + if err != nil { + t.Fatalf("%v:\n%s", err, cmd.Stderr) + } + got := strings.Trim(string(out), "\r\n") + t.Logf("in\n\t%s\n`pwd` reported\n\t%s", tc.dir, got) + if got != tc.want { + t.Errorf("want\n\t%s", tc.want) + } + }) + } +} + +// However, if cmd.Env is set explicitly, setting Dir should not override it. +// (This checks that the implementation for https://go.dev/issue/50599 doesn't +// break existing users who may have explicitly mismatched the PWD variable.) +func TestExplicitPWD(t *testing.T) { + t.Parallel() + + maySkipHelperCommand("pwd") + testenv.MustHaveSymlink(t) + + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + link := filepath.Join(t.TempDir(), "link") + if err := os.Symlink(cwd, link); err != nil { + t.Fatal(err) + } + + // Now link is another equally-valid name for cwd. If we set Dir to one and + // PWD to the other, the subprocess should report the PWD version. + cases := []struct { + name string + dir string + pwd string + }{ + {name: "original PWD", pwd: cwd}, + {name: "link PWD", pwd: link}, + {name: "in link with original PWD", dir: link, pwd: cwd}, + {name: "in dir with link PWD", dir: cwd, pwd: link}, + // Ideally we would also like to test what happens if we set PWD to + // something totally bogus (or the empty string), but then we would have no + // idea what output the subprocess should actually produce: cwd itself may + // contain symlinks preserved from the PWD value in the test's environment. + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pwd") + // This is intentionally opposite to the usual order of setting cmd.Dir + // and then calling cmd.Environ. Here, we *want* PWD not to match cmd.Dir, + // so we don't care whether cmd.Dir is reflected in cmd.Environ. + cmd.Env = append(cmd.Environ(), "PWD="+tc.pwd) + cmd.Dir = tc.dir + + var pwds []string + for _, kv := range cmd.Environ() { + if strings.HasPrefix(kv, "PWD=") { + pwds = append(pwds, strings.TrimPrefix(kv, "PWD=")) + } + } + + wantPWDs := []string{tc.pwd} + if !reflect.DeepEqual(pwds, wantPWDs) { + t.Errorf("PWD entries in cmd.Environ():\n\t%s\nwant:\n\t%s", strings.Join(pwds, "\n\t"), strings.Join(wantPWDs, "\n\t")) + } + + cmd.Stderr = new(strings.Builder) + out, err := cmd.Output() + if err != nil { + t.Fatalf("%v:\n%s", err, cmd.Stderr) + } + got := strings.Trim(string(out), "\r\n") + t.Logf("in\n\t%s\nwith PWD=%s\nsubprocess os.Getwd() reported\n\t%s", tc.dir, tc.pwd, got) + if got != tc.pwd { + t.Errorf("want\n\t%s", tc.pwd) + } + }) + } +} diff --git a/src/os/exec/exec_test.go b/src/os/exec/exec_test.go new file mode 100644 index 0000000..473f92b --- /dev/null +++ b/src/os/exec/exec_test.go @@ -0,0 +1,1784 @@ +// Copyright 2009 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. + +// Use an external test to avoid os/exec -> net/http -> crypto/x509 -> os/exec +// circular dependency on non-cgo darwin. + +package exec_test + +import ( + "bufio" + "bytes" + "context" + "errors" + "flag" + "fmt" + "internal/poll" + "internal/testenv" + "io" + "log" + "net" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "os/exec/internal/fdtest" + "os/signal" + "path/filepath" + "runtime" + "runtime/debug" + "strconv" + "strings" + "sync" + "sync/atomic" + "testing" + "time" +) + +// haveUnexpectedFDs is set at init time to report whether any file descriptors +// were open at program start. +var haveUnexpectedFDs bool + +func init() { + godebug := os.Getenv("GODEBUG") + if godebug != "" { + godebug += "," + } + godebug += "execwait=2" + os.Setenv("GODEBUG", godebug) + + if os.Getenv("GO_EXEC_TEST_PID") != "" { + return + } + if runtime.GOOS == "windows" { + return + } + for fd := uintptr(3); fd <= 100; fd++ { + if poll.IsPollDescriptor(fd) { + continue + } + + if fdtest.Exists(fd) { + haveUnexpectedFDs = true + return + } + } +} + +// TestMain allows the test binary to impersonate many other binaries, +// some of which may manipulate os.Stdin, os.Stdout, and/or os.Stderr +// (and thus cannot run as an ordinary Test function, since the testing +// package monkey-patches those variables before running tests). +func TestMain(m *testing.M) { + flag.Parse() + + pid := os.Getpid() + if os.Getenv("GO_EXEC_TEST_PID") == "" { + os.Setenv("GO_EXEC_TEST_PID", strconv.Itoa(pid)) + + code := m.Run() + if code == 0 && flag.Lookup("test.run").Value.String() == "" && flag.Lookup("test.list").Value.String() == "" { + for cmd := range helperCommands { + if _, ok := helperCommandUsed.Load(cmd); !ok { + fmt.Fprintf(os.Stderr, "helper command unused: %q\n", cmd) + code = 1 + } + } + } + + if !testing.Short() { + // Run a couple of GC cycles to increase the odds of detecting + // process leaks using the finalizers installed by GODEBUG=execwait=2. + runtime.GC() + runtime.GC() + } + + os.Exit(code) + } + + args := flag.Args() + if len(args) == 0 { + fmt.Fprintf(os.Stderr, "No command\n") + os.Exit(2) + } + + cmd, args := args[0], args[1:] + f, ok := helperCommands[cmd] + if !ok { + fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd) + os.Exit(2) + } + f(args...) + os.Exit(0) +} + +// registerHelperCommand registers a command that the test process can impersonate. +// A command should be registered in the same source file in which it is used. +// If all tests are run and pass, all registered commands must be used. +// (This prevents stale commands from accreting if tests are removed or +// refactored over time.) +func registerHelperCommand(name string, f func(...string)) { + if helperCommands[name] != nil { + panic("duplicate command registered: " + name) + } + helperCommands[name] = f +} + +// maySkipHelperCommand records that the test that uses the named helper command +// was invoked, but may call Skip on the test before actually calling +// helperCommand. +func maySkipHelperCommand(name string) { + helperCommandUsed.Store(name, true) +} + +// helperCommand returns an exec.Cmd that will run the named helper command. +func helperCommand(t *testing.T, name string, args ...string) *exec.Cmd { + t.Helper() + return helperCommandContext(t, nil, name, args...) +} + +// helperCommandContext is like helperCommand, but also accepts a Context under +// which to run the command. +func helperCommandContext(t *testing.T, ctx context.Context, name string, args ...string) (cmd *exec.Cmd) { + helperCommandUsed.LoadOrStore(name, true) + + t.Helper() + testenv.MustHaveExec(t) + + cs := append([]string{name}, args...) + if ctx != nil { + cmd = exec.CommandContext(ctx, exePath(t), cs...) + } else { + cmd = exec.Command(exePath(t), cs...) + } + return cmd +} + +// exePath returns the path to the running executable. +func exePath(t testing.TB) string { + exeOnce.Do(func() { + // Use os.Executable instead of os.Args[0] in case the caller modifies + // cmd.Dir: if the test binary is invoked like "./exec.test", it should + // not fail spuriously. + exeOnce.path, exeOnce.err = os.Executable() + }) + + if exeOnce.err != nil { + if t == nil { + panic(exeOnce.err) + } + t.Fatal(exeOnce.err) + } + + return exeOnce.path +} + +var exeOnce struct { + path string + err error + sync.Once +} + +var helperCommandUsed sync.Map + +var helperCommands = map[string]func(...string){ + "echo": cmdEcho, + "echoenv": cmdEchoEnv, + "cat": cmdCat, + "pipetest": cmdPipeTest, + "stdinClose": cmdStdinClose, + "exit": cmdExit, + "describefiles": cmdDescribeFiles, + "stderrfail": cmdStderrFail, + "yes": cmdYes, + "hang": cmdHang, +} + +func cmdEcho(args ...string) { + iargs := []any{} + for _, s := range args { + iargs = append(iargs, s) + } + fmt.Println(iargs...) +} + +func cmdEchoEnv(args ...string) { + for _, s := range args { + fmt.Println(os.Getenv(s)) + } +} + +func cmdCat(args ...string) { + if len(args) == 0 { + io.Copy(os.Stdout, os.Stdin) + return + } + exit := 0 + for _, fn := range args { + f, err := os.Open(fn) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + exit = 2 + } else { + defer f.Close() + io.Copy(os.Stdout, f) + } + } + os.Exit(exit) +} + +func cmdPipeTest(...string) { + bufr := bufio.NewReader(os.Stdin) + for { + line, _, err := bufr.ReadLine() + if err == io.EOF { + break + } else if err != nil { + os.Exit(1) + } + if bytes.HasPrefix(line, []byte("O:")) { + os.Stdout.Write(line) + os.Stdout.Write([]byte{'\n'}) + } else if bytes.HasPrefix(line, []byte("E:")) { + os.Stderr.Write(line) + os.Stderr.Write([]byte{'\n'}) + } else { + os.Exit(1) + } + } +} + +func cmdStdinClose(...string) { + b, err := io.ReadAll(os.Stdin) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + if s := string(b); s != stdinCloseTestString { + fmt.Fprintf(os.Stderr, "Error: Read %q, want %q", s, stdinCloseTestString) + os.Exit(1) + } +} + +func cmdExit(args ...string) { + n, _ := strconv.Atoi(args[0]) + os.Exit(n) +} + +func cmdDescribeFiles(args ...string) { + f := os.NewFile(3, fmt.Sprintf("fd3")) + ln, err := net.FileListener(f) + if err == nil { + fmt.Printf("fd3: listener %s\n", ln.Addr()) + ln.Close() + } +} + +func cmdStderrFail(...string) { + fmt.Fprintf(os.Stderr, "some stderr text\n") + os.Exit(1) +} + +func cmdYes(args ...string) { + if len(args) == 0 { + args = []string{"y"} + } + s := strings.Join(args, " ") + "\n" + for { + _, err := os.Stdout.WriteString(s) + if err != nil { + os.Exit(1) + } + } +} + +func TestEcho(t *testing.T) { + t.Parallel() + + bs, err := helperCommand(t, "echo", "foo bar", "baz").Output() + if err != nil { + t.Errorf("echo: %v", err) + } + if g, e := string(bs), "foo bar baz\n"; g != e { + t.Errorf("echo: want %q, got %q", e, g) + } +} + +func TestCommandRelativeName(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "echo", "foo") + + // Run our own binary as a relative path + // (e.g. "_test/exec.test") our parent directory. + base := filepath.Base(os.Args[0]) // "exec.test" + dir := filepath.Dir(os.Args[0]) // "/tmp/go-buildNNNN/os/exec/_test" + if dir == "." { + t.Skip("skipping; running test at root somehow") + } + parentDir := filepath.Dir(dir) // "/tmp/go-buildNNNN/os/exec" + dirBase := filepath.Base(dir) // "_test" + if dirBase == "." { + t.Skipf("skipping; unexpected shallow dir of %q", dir) + } + + cmd.Path = filepath.Join(dirBase, base) + cmd.Dir = parentDir + + out, err := cmd.Output() + if err != nil { + t.Errorf("echo: %v", err) + } + if g, e := string(out), "foo\n"; g != e { + t.Errorf("echo: want %q, got %q", e, g) + } +} + +func TestCatStdin(t *testing.T) { + t.Parallel() + + // Cat, testing stdin and stdout. + input := "Input string\nLine 2" + p := helperCommand(t, "cat") + p.Stdin = strings.NewReader(input) + bs, err := p.Output() + if err != nil { + t.Errorf("cat: %v", err) + } + s := string(bs) + if s != input { + t.Errorf("cat: want %q, got %q", input, s) + } +} + +func TestEchoFileRace(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "echo") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatalf("StdinPipe: %v", err) + } + if err := cmd.Start(); err != nil { + t.Fatalf("Start: %v", err) + } + wrote := make(chan bool) + go func() { + defer close(wrote) + fmt.Fprint(stdin, "echo\n") + }() + if err := cmd.Wait(); err != nil { + t.Fatalf("Wait: %v", err) + } + <-wrote +} + +func TestCatGoodAndBadFile(t *testing.T) { + t.Parallel() + + // Testing combined output and error values. + bs, err := helperCommand(t, "cat", "/bogus/file.foo", "exec_test.go").CombinedOutput() + if _, ok := err.(*exec.ExitError); !ok { + t.Errorf("expected *exec.ExitError from cat combined; got %T: %v", err, err) + } + errLine, body, ok := strings.Cut(string(bs), "\n") + if !ok { + t.Fatalf("expected two lines from cat; got %q", bs) + } + if !strings.HasPrefix(errLine, "Error: open /bogus/file.foo") { + t.Errorf("expected stderr to complain about file; got %q", errLine) + } + if !strings.Contains(body, "func TestCatGoodAndBadFile(t *testing.T)") { + t.Errorf("expected test code; got %q (len %d)", body, len(body)) + } +} + +func TestNoExistExecutable(t *testing.T) { + t.Parallel() + + // Can't run a non-existent executable + err := exec.Command("/no-exist-executable").Run() + if err == nil { + t.Error("expected error from /no-exist-executable") + } +} + +func TestExitStatus(t *testing.T) { + t.Parallel() + + // Test that exit values are returned correctly + cmd := helperCommand(t, "exit", "42") + err := cmd.Run() + want := "exit status 42" + switch runtime.GOOS { + case "plan9": + want = fmt.Sprintf("exit status: '%s %d: 42'", filepath.Base(cmd.Path), cmd.ProcessState.Pid()) + } + if werr, ok := err.(*exec.ExitError); ok { + if s := werr.Error(); s != want { + t.Errorf("from exit 42 got exit %q, want %q", s, want) + } + } else { + t.Fatalf("expected *exec.ExitError from exit 42; got %T: %v", err, err) + } +} + +func TestExitCode(t *testing.T) { + t.Parallel() + + // Test that exit code are returned correctly + cmd := helperCommand(t, "exit", "42") + cmd.Run() + want := 42 + if runtime.GOOS == "plan9" { + want = 1 + } + got := cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } + + cmd = helperCommand(t, "/no-exist-executable") + cmd.Run() + want = 2 + if runtime.GOOS == "plan9" { + want = 1 + } + got = cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } + + cmd = helperCommand(t, "exit", "255") + cmd.Run() + want = 255 + if runtime.GOOS == "plan9" { + want = 1 + } + got = cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } + + cmd = helperCommand(t, "cat") + cmd.Run() + want = 0 + got = cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } + + // Test when command does not call Run(). + cmd = helperCommand(t, "cat") + want = -1 + got = cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } +} + +func TestPipes(t *testing.T) { + t.Parallel() + + check := func(what string, err error) { + if err != nil { + t.Fatalf("%s: %v", what, err) + } + } + // Cat, testing stdin and stdout. + c := helperCommand(t, "pipetest") + stdin, err := c.StdinPipe() + check("StdinPipe", err) + stdout, err := c.StdoutPipe() + check("StdoutPipe", err) + stderr, err := c.StderrPipe() + check("StderrPipe", err) + + outbr := bufio.NewReader(stdout) + errbr := bufio.NewReader(stderr) + line := func(what string, br *bufio.Reader) string { + line, _, err := br.ReadLine() + if err != nil { + t.Fatalf("%s: %v", what, err) + } + return string(line) + } + + err = c.Start() + check("Start", err) + + _, err = stdin.Write([]byte("O:I am output\n")) + check("first stdin Write", err) + if g, e := line("first output line", outbr), "O:I am output"; g != e { + t.Errorf("got %q, want %q", g, e) + } + + _, err = stdin.Write([]byte("E:I am error\n")) + check("second stdin Write", err) + if g, e := line("first error line", errbr), "E:I am error"; g != e { + t.Errorf("got %q, want %q", g, e) + } + + _, err = stdin.Write([]byte("O:I am output2\n")) + check("third stdin Write 3", err) + if g, e := line("second output line", outbr), "O:I am output2"; g != e { + t.Errorf("got %q, want %q", g, e) + } + + stdin.Close() + err = c.Wait() + check("Wait", err) +} + +const stdinCloseTestString = "Some test string." + +// Issue 6270. +func TestStdinClose(t *testing.T) { + t.Parallel() + + check := func(what string, err error) { + if err != nil { + t.Fatalf("%s: %v", what, err) + } + } + cmd := helperCommand(t, "stdinClose") + stdin, err := cmd.StdinPipe() + check("StdinPipe", err) + // Check that we can access methods of the underlying os.File.` + if _, ok := stdin.(interface { + Fd() uintptr + }); !ok { + t.Error("can't access methods of underlying *os.File") + } + check("Start", cmd.Start()) + + var wg sync.WaitGroup + wg.Add(1) + defer wg.Wait() + go func() { + defer wg.Done() + + _, err := io.Copy(stdin, strings.NewReader(stdinCloseTestString)) + check("Copy", err) + + // Before the fix, this next line would race with cmd.Wait. + if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) { + t.Errorf("Close: %v", err) + } + }() + + check("Wait", cmd.Wait()) +} + +// Issue 17647. +// It used to be the case that TestStdinClose, above, would fail when +// run under the race detector. This test is a variant of TestStdinClose +// that also used to fail when run under the race detector. +// This test is run by cmd/dist under the race detector to verify that +// the race detector no longer reports any problems. +func TestStdinCloseRace(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "stdinClose") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatalf("StdinPipe: %v", err) + } + if err := cmd.Start(); err != nil { + t.Fatalf("Start: %v", err) + + } + + var wg sync.WaitGroup + wg.Add(2) + defer wg.Wait() + + go func() { + defer wg.Done() + // We don't check the error return of Kill. It is + // possible that the process has already exited, in + // which case Kill will return an error "process + // already finished". The purpose of this test is to + // see whether the race detector reports an error; it + // doesn't matter whether this Kill succeeds or not. + cmd.Process.Kill() + }() + + go func() { + defer wg.Done() + // Send the wrong string, so that the child fails even + // if the other goroutine doesn't manage to kill it first. + // This test is to check that the race detector does not + // falsely report an error, so it doesn't matter how the + // child process fails. + io.Copy(stdin, strings.NewReader("unexpected string")) + if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) { + t.Errorf("stdin.Close: %v", err) + } + }() + + if err := cmd.Wait(); err == nil { + t.Fatalf("Wait: succeeded unexpectedly") + } +} + +// Issue 5071 +func TestPipeLookPathLeak(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("we don't currently suppore counting open handles on windows") + } + // Not parallel: checks for leaked file descriptors + + openFDs := func() []uintptr { + var fds []uintptr + for i := uintptr(0); i < 100; i++ { + if fdtest.Exists(i) { + fds = append(fds, i) + } + } + return fds + } + + old := map[uintptr]bool{} + for _, fd := range openFDs() { + old[fd] = true + } + + for i := 0; i < 6; i++ { + cmd := exec.Command("something-that-does-not-exist-executable") + cmd.StdoutPipe() + cmd.StderrPipe() + cmd.StdinPipe() + if err := cmd.Run(); err == nil { + t.Fatal("unexpected success") + } + } + + // Since this test is not running in parallel, we don't expect any new file + // descriptors to be opened while it runs. However, if there are additional + // FDs present at the start of the test (for example, opened by libc), those + // may be closed due to a timeout of some sort. Allow those to go away, but + // check that no new FDs are added. + for _, fd := range openFDs() { + if !old[fd] { + t.Errorf("leaked file descriptor %v", fd) + } + } +} + +func TestExtraFiles(t *testing.T) { + if testing.Short() { + t.Skipf("skipping test in short mode that would build a helper binary") + } + + if haveUnexpectedFDs { + // The point of this test is to make sure that any + // descriptors we open are marked close-on-exec. + // If haveUnexpectedFDs is true then there were other + // descriptors open when we started the test, + // so those descriptors are clearly not close-on-exec, + // and they will confuse the test. We could modify + // the test to expect those descriptors to remain open, + // but since we don't know where they came from or what + // they are doing, that seems fragile. For example, + // perhaps they are from the startup code on this + // system for some reason. Also, this test is not + // system-specific; as long as most systems do not skip + // the test, we will still be testing what we care about. + t.Skip("skipping test because test was run with FDs open") + } + + testenv.MustHaveExec(t) + testenv.MustHaveGoBuild(t) + + // This test runs with cgo disabled. External linking needs cgo, so + // it doesn't work if external linking is required. + testenv.MustInternalLink(t, false) + + if runtime.GOOS == "windows" { + t.Skipf("skipping test on %q", runtime.GOOS) + } + + // Force network usage, to verify the epoll (or whatever) fd + // doesn't leak to the child, + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + + // Make sure duplicated fds don't leak to the child. + f, err := ln.(*net.TCPListener).File() + if err != nil { + t.Fatal(err) + } + defer f.Close() + ln2, err := net.FileListener(f) + if err != nil { + t.Fatal(err) + } + defer ln2.Close() + + // Force TLS root certs to be loaded (which might involve + // cgo), to make sure none of that potential C code leaks fds. + ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + // quiet expected TLS handshake error "remote error: bad certificate" + ts.Config.ErrorLog = log.New(io.Discard, "", 0) + ts.StartTLS() + defer ts.Close() + _, err = http.Get(ts.URL) + if err == nil { + t.Errorf("success trying to fetch %s; want an error", ts.URL) + } + + tf, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("TempFile: %v", err) + } + defer os.Remove(tf.Name()) + defer tf.Close() + + const text = "Hello, fd 3!" + _, err = tf.Write([]byte(text)) + if err != nil { + t.Fatalf("Write: %v", err) + } + _, err = tf.Seek(0, io.SeekStart) + if err != nil { + t.Fatalf("Seek: %v", err) + } + + tempdir := t.TempDir() + exe := filepath.Join(tempdir, "read3.exe") + + c := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", exe, "read3.go") + // Build the test without cgo, so that C library functions don't + // open descriptors unexpectedly. See issue 25628. + c.Env = append(os.Environ(), "CGO_ENABLED=0") + if output, err := c.CombinedOutput(); err != nil { + t.Logf("go build -o %s read3.go\n%s", exe, output) + t.Fatalf("go build failed: %v", err) + } + + // Use a deadline to try to get some output even if the program hangs. + ctx := context.Background() + if deadline, ok := t.Deadline(); ok { + // Leave a 20% grace period to flush output, which may be large on the + // linux/386 builders because we're running the subprocess under strace. + deadline = deadline.Add(-time.Until(deadline) / 5) + + var cancel context.CancelFunc + ctx, cancel = context.WithDeadline(ctx, deadline) + defer cancel() + } + + c = exec.CommandContext(ctx, exe) + var stdout, stderr strings.Builder + c.Stdout = &stdout + c.Stderr = &stderr + c.ExtraFiles = []*os.File{tf} + if runtime.GOOS == "illumos" { + // Some facilities in illumos are implemented via access + // to /proc by libc; such accesses can briefly occupy a + // low-numbered fd. If this occurs concurrently with the + // test that checks for leaked descriptors, the check can + // become confused and report a spurious leaked descriptor. + // (See issue #42431 for more detailed analysis.) + // + // Attempt to constrain the use of additional threads in the + // child process to make this test less flaky: + c.Env = append(os.Environ(), "GOMAXPROCS=1") + } + err = c.Run() + if err != nil { + t.Fatalf("Run: %v\n--- stdout:\n%s--- stderr:\n%s", err, stdout.String(), stderr.String()) + } + if stdout.String() != text { + t.Errorf("got stdout %q, stderr %q; want %q on stdout", stdout.String(), stderr.String(), text) + } +} + +func TestExtraFilesRace(t *testing.T) { + if runtime.GOOS == "windows" { + maySkipHelperCommand("describefiles") + t.Skip("no operating system support; skipping") + } + t.Parallel() + + listen := func() net.Listener { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + return ln + } + listenerFile := func(ln net.Listener) *os.File { + f, err := ln.(*net.TCPListener).File() + if err != nil { + t.Fatal(err) + } + return f + } + runCommand := func(c *exec.Cmd, out chan<- string) { + bout, err := c.CombinedOutput() + if err != nil { + out <- "ERROR:" + err.Error() + } else { + out <- string(bout) + } + } + + for i := 0; i < 10; i++ { + if testing.Short() && i >= 3 { + break + } + la := listen() + ca := helperCommand(t, "describefiles") + ca.ExtraFiles = []*os.File{listenerFile(la)} + lb := listen() + cb := helperCommand(t, "describefiles") + cb.ExtraFiles = []*os.File{listenerFile(lb)} + ares := make(chan string) + bres := make(chan string) + go runCommand(ca, ares) + go runCommand(cb, bres) + if got, want := <-ares, fmt.Sprintf("fd3: listener %s\n", la.Addr()); got != want { + t.Errorf("iteration %d, process A got:\n%s\nwant:\n%s\n", i, got, want) + } + if got, want := <-bres, fmt.Sprintf("fd3: listener %s\n", lb.Addr()); got != want { + t.Errorf("iteration %d, process B got:\n%s\nwant:\n%s\n", i, got, want) + } + la.Close() + lb.Close() + for _, f := range ca.ExtraFiles { + f.Close() + } + for _, f := range cb.ExtraFiles { + f.Close() + } + } +} + +type delayedInfiniteReader struct{} + +func (delayedInfiniteReader) Read(b []byte) (int, error) { + time.Sleep(100 * time.Millisecond) + for i := range b { + b[i] = 'x' + } + return len(b), nil +} + +// Issue 9173: ignore stdin pipe writes if the program completes successfully. +func TestIgnorePipeErrorOnSuccess(t *testing.T) { + t.Parallel() + + testWith := func(r io.Reader) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "echo", "foo") + var out strings.Builder + cmd.Stdin = r + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + t.Fatal(err) + } + if got, want := out.String(), "foo\n"; got != want { + t.Errorf("output = %q; want %q", got, want) + } + } + } + t.Run("10MB", testWith(strings.NewReader(strings.Repeat("x", 10<<20)))) + t.Run("Infinite", testWith(delayedInfiniteReader{})) +} + +type badWriter struct{} + +func (w *badWriter) Write(data []byte) (int, error) { + return 0, io.ErrUnexpectedEOF +} + +func TestClosePipeOnCopyError(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "yes") + cmd.Stdout = new(badWriter) + err := cmd.Run() + if err == nil { + t.Errorf("yes unexpectedly completed successfully") + } +} + +func TestOutputStderrCapture(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "stderrfail") + _, err := cmd.Output() + ee, ok := err.(*exec.ExitError) + if !ok { + t.Fatalf("Output error type = %T; want ExitError", err) + } + got := string(ee.Stderr) + want := "some stderr text\n" + if got != want { + t.Errorf("ExitError.Stderr = %q; want %q", got, want) + } +} + +func TestContext(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + c := helperCommandContext(t, ctx, "pipetest") + stdin, err := c.StdinPipe() + if err != nil { + t.Fatal(err) + } + stdout, err := c.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := c.Start(); err != nil { + t.Fatal(err) + } + + if _, err := stdin.Write([]byte("O:hi\n")); err != nil { + t.Fatal(err) + } + buf := make([]byte, 5) + n, err := io.ReadFull(stdout, buf) + if n != len(buf) || err != nil || string(buf) != "O:hi\n" { + t.Fatalf("ReadFull = %d, %v, %q", n, err, buf[:n]) + } + go cancel() + + if err := c.Wait(); err == nil { + t.Fatal("expected Wait failure") + } +} + +func TestContextCancel(t *testing.T) { + if runtime.GOOS == "netbsd" && runtime.GOARCH == "arm64" { + maySkipHelperCommand("cat") + testenv.SkipFlaky(t, 42061) + } + + // To reduce noise in the final goroutine dump, + // let other parallel tests complete if possible. + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + c := helperCommandContext(t, ctx, "cat") + + stdin, err := c.StdinPipe() + if err != nil { + t.Fatal(err) + } + defer stdin.Close() + + if err := c.Start(); err != nil { + t.Fatal(err) + } + + // At this point the process is alive. Ensure it by sending data to stdin. + if _, err := io.WriteString(stdin, "echo"); err != nil { + t.Fatal(err) + } + + cancel() + + // Calling cancel should have killed the process, so writes + // should now fail. Give the process a little while to die. + start := time.Now() + delay := 1 * time.Millisecond + for { + if _, err := io.WriteString(stdin, "echo"); err != nil { + break + } + + if time.Since(start) > time.Minute { + // Panic instead of calling t.Fatal so that we get a goroutine dump. + // We want to know exactly what the os/exec goroutines got stuck on. + debug.SetTraceback("system") + panic("canceling context did not stop program") + } + + // Back off exponentially (up to 1-second sleeps) to give the OS time to + // terminate the process. + delay *= 2 + if delay > 1*time.Second { + delay = 1 * time.Second + } + time.Sleep(delay) + } + + if err := c.Wait(); err == nil { + t.Error("program unexpectedly exited successfully") + } else { + t.Logf("exit status: %v", err) + } +} + +// test that environment variables are de-duped. +func TestDedupEnvEcho(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "echoenv", "FOO") + cmd.Env = append(cmd.Environ(), "FOO=bad", "FOO=good") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + if got, want := strings.TrimSpace(string(out)), "good"; got != want { + t.Errorf("output = %q; want %q", got, want) + } +} + +func TestEnvNULCharacter(t *testing.T) { + if runtime.GOOS == "plan9" { + t.Skip("plan9 explicitly allows NUL in the environment") + } + cmd := helperCommand(t, "echoenv", "FOO", "BAR") + cmd.Env = append(cmd.Environ(), "FOO=foo\x00BAR=bar") + out, err := cmd.CombinedOutput() + if err == nil { + t.Errorf("output = %q; want error", string(out)) + } +} + +func TestString(t *testing.T) { + t.Parallel() + + echoPath, err := exec.LookPath("echo") + if err != nil { + t.Skip(err) + } + tests := [...]struct { + path string + args []string + want string + }{ + {"echo", nil, echoPath}, + {"echo", []string{"a"}, echoPath + " a"}, + {"echo", []string{"a", "b"}, echoPath + " a b"}, + } + for _, test := range tests { + cmd := exec.Command(test.path, test.args...) + if got := cmd.String(); got != test.want { + t.Errorf("String(%q, %q) = %q, want %q", test.path, test.args, got, test.want) + } + } +} + +func TestStringPathNotResolved(t *testing.T) { + t.Parallel() + + _, err := exec.LookPath("makemeasandwich") + if err == nil { + t.Skip("wow, thanks") + } + + cmd := exec.Command("makemeasandwich", "-lettuce") + want := "makemeasandwich -lettuce" + if got := cmd.String(); got != want { + t.Errorf("String(%q, %q) = %q, want %q", "makemeasandwich", "-lettuce", got, want) + } +} + +func TestNoPath(t *testing.T) { + err := new(exec.Cmd).Start() + want := "exec: no command" + if err == nil || err.Error() != want { + t.Errorf("new(Cmd).Start() = %v, want %q", err, want) + } +} + +// TestDoubleStartLeavesPipesOpen checks for a regression in which calling +// Start twice, which returns an error on the second call, would spuriously +// close the pipes established in the first call. +func TestDoubleStartLeavesPipesOpen(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pipetest") + in, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + out, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := cmd.Wait(); err != nil { + t.Error(err) + } + }) + + if err := cmd.Start(); err == nil || !strings.HasSuffix(err.Error(), "already started") { + t.Fatalf("second call to Start returned a nil; want an 'already started' error") + } + + outc := make(chan []byte, 1) + go func() { + b, err := io.ReadAll(out) + if err != nil { + t.Error(err) + } + outc <- b + }() + + const msg = "O:Hello, pipe!\n" + + _, err = io.WriteString(in, msg) + if err != nil { + t.Fatal(err) + } + in.Close() + + b := <-outc + if !bytes.Equal(b, []byte(msg)) { + t.Fatalf("read %q from stdout pipe; want %q", b, msg) + } +} + +func cmdHang(args ...string) { + sleep, err := time.ParseDuration(args[0]) + if err != nil { + panic(err) + } + + fs := flag.NewFlagSet("hang", flag.ExitOnError) + exitOnInterrupt := fs.Bool("interrupt", false, "if true, commands should exit 0 on os.Interrupt") + subsleep := fs.Duration("subsleep", 0, "amount of time for the 'hang' helper to leave an orphaned subprocess sleeping with stderr open") + probe := fs.Duration("probe", 0, "if nonzero, the 'hang' helper should write to stderr at this interval, and exit nonzero if a write fails") + read := fs.Bool("read", false, "if true, the 'hang' helper should read stdin to completion before sleeping") + fs.Parse(args[1:]) + + pid := os.Getpid() + + if *subsleep != 0 { + cmd := exec.Command(exePath(nil), "hang", subsleep.String(), "-read=true", "-probe="+probe.String()) + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + out, err := cmd.StdoutPipe() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + cmd.Start() + + buf := new(strings.Builder) + if _, err := io.Copy(buf, out); err != nil { + fmt.Fprintln(os.Stderr, err) + cmd.Process.Kill() + cmd.Wait() + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "%d: started %d: %v\n", pid, cmd.Process.Pid, cmd) + go cmd.Wait() // Release resources if cmd happens not to outlive this process. + } + + if *exitOnInterrupt { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + sig := <-c + fmt.Fprintf(os.Stderr, "%d: received %v\n", pid, sig) + os.Exit(0) + }() + } else { + signal.Ignore(os.Interrupt) + } + + // Signal that the process is set up by closing stdout. + os.Stdout.Close() + + if *read { + if pipeSignal != nil { + signal.Ignore(pipeSignal) + } + r := bufio.NewReader(os.Stdin) + for { + line, err := r.ReadBytes('\n') + if len(line) > 0 { + // Ignore write errors: we want to keep reading even if stderr is closed. + fmt.Fprintf(os.Stderr, "%d: read %s", pid, line) + } + if err != nil { + fmt.Fprintf(os.Stderr, "%d: finished read: %v", pid, err) + break + } + } + } + + if *probe != 0 { + ticker := time.NewTicker(*probe) + go func() { + for range ticker.C { + if _, err := fmt.Fprintf(os.Stderr, "%d: ok\n", pid); err != nil { + os.Exit(1) + } + } + }() + } + + if sleep != 0 { + time.Sleep(sleep) + fmt.Fprintf(os.Stderr, "%d: slept %v\n", pid, sleep) + } +} + +// A tickReader reads an unbounded sequence of timestamps at no more than a +// fixed interval. +type tickReader struct { + interval time.Duration + lastTick time.Time + s string +} + +func newTickReader(interval time.Duration) *tickReader { + return &tickReader{interval: interval} +} + +func (r *tickReader) Read(p []byte) (n int, err error) { + if len(r.s) == 0 { + if d := r.interval - time.Since(r.lastTick); d > 0 { + time.Sleep(d) + } + r.lastTick = time.Now() + r.s = r.lastTick.Format(time.RFC3339Nano + "\n") + } + + n = copy(p, r.s) + r.s = r.s[n:] + return n, nil +} + +func startHang(t *testing.T, ctx context.Context, hangTime time.Duration, interrupt os.Signal, waitDelay time.Duration, flags ...string) *exec.Cmd { + t.Helper() + + args := append([]string{hangTime.String()}, flags...) + cmd := helperCommandContext(t, ctx, "hang", args...) + cmd.Stdin = newTickReader(1 * time.Millisecond) + cmd.Stderr = new(strings.Builder) + if interrupt == nil { + cmd.Cancel = nil + } else { + cmd.Cancel = func() error { + return cmd.Process.Signal(interrupt) + } + } + cmd.WaitDelay = waitDelay + out, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + + t.Log(cmd) + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + // Wait for cmd to close stdout to signal that its handlers are installed. + buf := new(strings.Builder) + if _, err := io.Copy(buf, out); err != nil { + t.Error(err) + cmd.Process.Kill() + cmd.Wait() + t.FailNow() + } + if buf.Len() > 0 { + t.Logf("stdout %v:\n%s", cmd.Args, buf) + } + + return cmd +} + +func TestWaitInterrupt(t *testing.T) { + t.Parallel() + + // tooLong is an arbitrary duration that is expected to be much longer than + // the test runs, but short enough that leaked processes will eventually exit + // on their own. + const tooLong = 10 * time.Minute + + // Control case: with no cancellation and no WaitDelay, we should wait for the + // process to exit. + t.Run("Wait", func(t *testing.T) { + t.Parallel() + cmd := startHang(t, context.Background(), 1*time.Millisecond, os.Kill, 0) + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + if err != nil { + t.Errorf("Wait: %v; want ", err) + } + if ps := cmd.ProcessState; !ps.Exited() { + t.Errorf("cmd did not exit: %v", ps) + } else if code := ps.ExitCode(); code != 0 { + t.Errorf("cmd.ProcessState.ExitCode() = %v; want 0", code) + } + }) + + // With a very long WaitDelay and no Cancel function, we should wait for the + // process to exit even if the command's Context is cancelled. + t.Run("WaitDelay", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("skipping: os.Interrupt is not implemented on Windows") + } + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, nil, tooLong, "-interrupt=true") + cancel() + + time.Sleep(1 * time.Millisecond) + // At this point cmd should still be running (because we passed nil to + // startHang for the cancel signal). Sending it an explicit Interrupt signal + // should succeed. + if err := cmd.Process.Signal(os.Interrupt); err != nil { + t.Error(err) + } + + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // This program exits with status 0, + // but pretty much always does so during the wait delay. + // Since the Cmd itself didn't do anything to stop the process when the + // context expired, a successful exit is valid (even if late) and does + // not merit a non-nil error. + if err != nil { + t.Errorf("Wait: %v; want nil", err) + } + if ps := cmd.ProcessState; !ps.Exited() { + t.Errorf("cmd did not exit: %v", ps) + } else if code := ps.ExitCode(); code != 0 { + t.Errorf("cmd.ProcessState.ExitCode() = %v; want 0", code) + } + }) + + // If the context is cancelled and the Cancel function sends os.Kill, + // the process should be terminated immediately, and its output + // pipes should be closed (causing Wait to return) after WaitDelay + // even if a child process is still writing to them. + t.Run("SIGKILL-hang", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, os.Kill, 10*time.Millisecond, "-subsleep=10m", "-probe=1ms") + cancel() + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // This test should kill the child process after 10ms, + // leaving a grandchild process writing probes in a loop. + // The child process should be reported as failed, + // and the grandchild will exit (or die by SIGPIPE) once the + // stderr pipe is closed. + if ee := new(*exec.ExitError); !errors.As(err, ee) { + t.Errorf("Wait error = %v; want %T", err, *ee) + } + }) + + // If the process exits with status 0 but leaves a child behind writing + // to its output pipes, Wait should only wait for WaitDelay before + // closing the pipes and returning. Wait should return ErrWaitDelay + // to indicate that the piped output may be incomplete even though the + // command returned a “success” code. + t.Run("Exit-hang", func(t *testing.T) { + t.Parallel() + + cmd := startHang(t, context.Background(), 1*time.Millisecond, nil, 10*time.Millisecond, "-subsleep=10m", "-probe=1ms") + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // This child process should exit immediately, + // leaving a grandchild process writing probes in a loop. + // Since the child has no ExitError to report but we did not + // read all of its output, Wait should return ErrWaitDelay. + if !errors.Is(err, exec.ErrWaitDelay) { + t.Errorf("Wait error = %v; want %T", err, exec.ErrWaitDelay) + } + }) + + // If the Cancel function sends a signal that the process can handle, and it + // handles that signal without actually exiting, then it should be terminated + // after the WaitDelay. + t.Run("SIGINT-ignored", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("skipping: os.Interrupt is not implemented on Windows") + } + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, os.Interrupt, 10*time.Millisecond, "-interrupt=false") + cancel() + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // This command ignores SIGINT, sleeping until it is killed. + // Wait should return the usual error for a killed process. + if ee := new(*exec.ExitError); !errors.As(err, ee) { + t.Errorf("Wait error = %v; want %T", err, *ee) + } + }) + + // If the process handles the cancellation signal and exits with status 0, + // Wait should report a non-nil error (because the process had to be + // interrupted), and it should be a context error (because there is no error + // to report from the child process itself). + t.Run("SIGINT-handled", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("skipping: os.Interrupt is not implemented on Windows") + } + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, os.Interrupt, 0, "-interrupt=true") + cancel() + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + if !errors.Is(err, ctx.Err()) { + t.Errorf("Wait error = %v; want %v", err, ctx.Err()) + } + if ps := cmd.ProcessState; !ps.Exited() { + t.Errorf("cmd did not exit: %v", ps) + } else if code := ps.ExitCode(); code != 0 { + t.Errorf("cmd.ProcessState.ExitCode() = %v; want 0", code) + } + }) + + // If the Cancel function sends SIGQUIT, it should be handled in the usual + // way: a Go program should dump its goroutines and exit with non-success + // status. (We expect SIGQUIT to be a common pattern in real-world use.) + t.Run("SIGQUIT", func(t *testing.T) { + if quitSignal == nil { + t.Skipf("skipping: SIGQUIT is not supported on %v", runtime.GOOS) + } + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, quitSignal, 0) + cancel() + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + if ee := new(*exec.ExitError); !errors.As(err, ee) { + t.Errorf("Wait error = %v; want %v", err, ctx.Err()) + } + + if ps := cmd.ProcessState; !ps.Exited() { + t.Errorf("cmd did not exit: %v", ps) + } else if code := ps.ExitCode(); code != 2 { + // The default os/signal handler exits with code 2. + t.Errorf("cmd.ProcessState.ExitCode() = %v; want 2", code) + } + + if !strings.Contains(fmt.Sprint(cmd.Stderr), "\n\ngoroutine ") { + t.Errorf("cmd.Stderr does not contain a goroutine dump") + } + }) +} + +func TestCancelErrors(t *testing.T) { + t.Parallel() + + // If Cancel returns a non-ErrProcessDone error and the process + // exits successfully, Wait should wrap the error from Cancel. + t.Run("success after error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + + errArbitrary := errors.New("arbitrary error") + cmd.Cancel = func() error { + stdin.Close() + t.Logf("Cancel returning %v", errArbitrary) + return errArbitrary + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + cancel() + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + if !errors.Is(err, errArbitrary) || err == errArbitrary { + t.Errorf("Wait error = %v; want an error wrapping %v", err, errArbitrary) + } + }) + + // If Cancel returns an error equivalent to ErrProcessDone, + // Wait should ignore that error. (ErrProcessDone indicates that the + // process was already done before we tried to interrupt it — maybe we + // just didn't notice because Wait hadn't been called yet.) + t.Run("success after ErrProcessDone", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + + // We intentionally race Cancel against the process exiting, + // but ensure that the process wins the race (and return ErrProcessDone + // from Cancel to report that). + interruptCalled := make(chan struct{}) + done := make(chan struct{}) + cmd.Cancel = func() error { + close(interruptCalled) + <-done + t.Logf("Cancel returning an error wrapping ErrProcessDone") + return fmt.Errorf("%w: stdout closed", os.ErrProcessDone) + } + + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + cancel() + <-interruptCalled + stdin.Close() + io.Copy(io.Discard, stdout) // reaches EOF when the process exits + close(done) + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + if err != nil { + t.Errorf("Wait error = %v; want nil", err) + } + }) + + // If Cancel returns an error and the process is killed after + // WaitDelay, Wait should report the usual SIGKILL ExitError, not the + // error from Cancel. + t.Run("killed after error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + defer stdin.Close() + + errArbitrary := errors.New("arbitrary error") + var interruptCalled atomic.Bool + cmd.Cancel = func() error { + t.Logf("Cancel called") + interruptCalled.Store(true) + return errArbitrary + } + cmd.WaitDelay = 1 * time.Millisecond + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + cancel() + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // Ensure that Cancel actually had the opportunity to + // return the error. + if !interruptCalled.Load() { + t.Errorf("Cancel was not called when the context was canceled") + } + + // This test should kill the child process after 1ms, + // To maximize compatibility with existing uses of exec.CommandContext, the + // resulting error should be an exec.ExitError without additional wrapping. + if ee, ok := err.(*exec.ExitError); !ok { + t.Errorf("Wait error = %v; want %T", err, *ee) + } + }) + + // If Cancel returns ErrProcessDone but the process is not actually done + // (and has to be killed), Wait should report the usual SIGKILL ExitError, + // not the error from Cancel. + t.Run("killed after spurious ErrProcessDone", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + defer stdin.Close() + + var interruptCalled atomic.Bool + cmd.Cancel = func() error { + t.Logf("Cancel returning an error wrapping ErrProcessDone") + interruptCalled.Store(true) + return fmt.Errorf("%w: stdout closed", os.ErrProcessDone) + } + cmd.WaitDelay = 1 * time.Millisecond + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + cancel() + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // Ensure that Cancel actually had the opportunity to + // return the error. + if !interruptCalled.Load() { + t.Errorf("Cancel was not called when the context was canceled") + } + + // This test should kill the child process after 1ms, + // To maximize compatibility with existing uses of exec.CommandContext, the + // resulting error should be an exec.ExitError without additional wrapping. + if ee, ok := err.(*exec.ExitError); !ok { + t.Errorf("Wait error of type %T; want %T", err, ee) + } + }) + + // If Cancel returns an error and the process exits with an + // unsuccessful exit code, the process error should take precedence over the + // Cancel error. + t.Run("nonzero exit after error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "stderrfail") + stderr, err := cmd.StderrPipe() + if err != nil { + t.Fatal(err) + } + + errArbitrary := errors.New("arbitrary error") + interrupted := make(chan struct{}) + cmd.Cancel = func() error { + close(interrupted) + return errArbitrary + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + cancel() + <-interrupted + io.Copy(io.Discard, stderr) + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + + if ee, ok := err.(*exec.ExitError); !ok || ee.ProcessState.ExitCode() != 1 { + t.Errorf("Wait error = %v; want exit status 1", err) + } + }) +} + +// TestConcurrentExec is a regression test for https://go.dev/issue/61080. +// +// Forking multiple child processes concurrently would sometimes hang on darwin. +// (This test hung on a gomote with -count=100 after only a few iterations.) +func TestConcurrentExec(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + // This test will spawn nHangs subprocesses that hang reading from stdin, + // and nExits subprocesses that exit immediately. + // + // When issue #61080 was present, a long-lived "hang" subprocess would + // occasionally inherit the fork/exec status pipe from an "exit" subprocess, + // causing the parent process (which expects to see an EOF on that pipe almost + // immediately) to unexpectedly block on reading from the pipe. + var ( + nHangs = runtime.GOMAXPROCS(0) + nExits = runtime.GOMAXPROCS(0) + hangs, exits sync.WaitGroup + ) + hangs.Add(nHangs) + exits.Add(nExits) + + // ready is done when the goroutines have done as much work as possible to + // prepare to create subprocesses. It isn't strictly necessary for the test, + // but helps to increase the repro rate by making it more likely that calls to + // syscall.StartProcess for the "hang" and "exit" goroutines overlap. + var ready sync.WaitGroup + ready.Add(nHangs + nExits) + + for i := 0; i < nHangs; i++ { + go func() { + defer hangs.Done() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + ready.Done() + t.Error(err) + return + } + cmd.Cancel = stdin.Close + ready.Done() + + ready.Wait() + if err := cmd.Start(); err != nil { + if !errors.Is(err, context.Canceled) { + t.Error(err) + } + return + } + + cmd.Wait() + }() + } + + for i := 0; i < nExits; i++ { + go func() { + defer exits.Done() + + cmd := helperCommandContext(t, ctx, "exit", "0") + ready.Done() + + ready.Wait() + if err := cmd.Run(); err != nil { + t.Error(err) + } + }() + } + + exits.Wait() + cancel() + hangs.Wait() +} diff --git a/src/os/exec/exec_unix.go b/src/os/exec/exec_unix.go new file mode 100644 index 0000000..3ed672a --- /dev/null +++ b/src/os/exec/exec_unix.go @@ -0,0 +1,24 @@ +// Copyright 2015 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. + +//go:build !plan9 && !windows + +package exec + +import ( + "io/fs" + "syscall" +) + +// skipStdinCopyError optionally specifies a function which reports +// whether the provided stdin copy error should be ignored. +func skipStdinCopyError(err error) bool { + // Ignore EPIPE errors copying to stdin if the program + // completed successfully otherwise. + // See Issue 9173. + pe, ok := err.(*fs.PathError) + return ok && + pe.Op == "write" && pe.Path == "|1" && + pe.Err == syscall.EPIPE +} diff --git a/src/os/exec/exec_unix_test.go b/src/os/exec/exec_unix_test.go new file mode 100644 index 0000000..d26c93a --- /dev/null +++ b/src/os/exec/exec_unix_test.go @@ -0,0 +1,17 @@ +// Copyright 2022 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. + +//go:build unix + +package exec_test + +import ( + "os" + "syscall" +) + +var ( + quitSignal os.Signal = syscall.SIGQUIT + pipeSignal os.Signal = syscall.SIGPIPE +) diff --git a/src/os/exec/exec_windows.go b/src/os/exec/exec_windows.go new file mode 100644 index 0000000..e7a2ee6 --- /dev/null +++ b/src/os/exec/exec_windows.go @@ -0,0 +1,23 @@ +// Copyright 2017 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 exec + +import ( + "io/fs" + "syscall" +) + +// skipStdinCopyError optionally specifies a function which reports +// whether the provided stdin copy error should be ignored. +func skipStdinCopyError(err error) bool { + // Ignore ERROR_BROKEN_PIPE and ERROR_NO_DATA errors copying + // to stdin if the program completed successfully otherwise. + // See Issue 20445. + const _ERROR_NO_DATA = syscall.Errno(0xe8) + pe, ok := err.(*fs.PathError) + return ok && + pe.Op == "write" && pe.Path == "|1" && + (pe.Err == syscall.ERROR_BROKEN_PIPE || pe.Err == _ERROR_NO_DATA) +} diff --git a/src/os/exec/exec_windows_test.go b/src/os/exec/exec_windows_test.go new file mode 100644 index 0000000..efd3710 --- /dev/null +++ b/src/os/exec/exec_windows_test.go @@ -0,0 +1,109 @@ +// Copyright 2021 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. + +//go:build windows + +package exec_test + +import ( + "fmt" + "internal/testenv" + "io" + "os" + "os/exec" + "strconv" + "strings" + "syscall" + "testing" +) + +var ( + quitSignal os.Signal = nil + pipeSignal os.Signal = syscall.SIGPIPE +) + +func init() { + registerHelperCommand("pipehandle", cmdPipeHandle) +} + +func cmdPipeHandle(args ...string) { + handle, _ := strconv.ParseUint(args[0], 16, 64) + pipe := os.NewFile(uintptr(handle), "") + _, err := fmt.Fprint(pipe, args[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "writing to pipe failed: %v\n", err) + os.Exit(1) + } + pipe.Close() +} + +func TestPipePassing(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Error(err) + } + const marker = "arrakis, dune, desert planet" + childProc := helperCommand(t, "pipehandle", strconv.FormatUint(uint64(w.Fd()), 16), marker) + childProc.SysProcAttr = &syscall.SysProcAttr{AdditionalInheritedHandles: []syscall.Handle{syscall.Handle(w.Fd())}} + err = childProc.Start() + if err != nil { + t.Error(err) + } + w.Close() + response, err := io.ReadAll(r) + if err != nil { + t.Error(err) + } + r.Close() + if string(response) != marker { + t.Errorf("got %q; want %q", string(response), marker) + } + err = childProc.Wait() + if err != nil { + t.Error(err) + } +} + +func TestNoInheritHandles(t *testing.T) { + t.Parallel() + + cmd := testenv.Command(t, "cmd", "/c exit 88") + cmd.SysProcAttr = &syscall.SysProcAttr{NoInheritHandles: true} + err := cmd.Run() + exitError, ok := err.(*exec.ExitError) + if !ok { + t.Fatalf("got error %v; want ExitError", err) + } + if exitError.ExitCode() != 88 { + t.Fatalf("got exit code %d; want 88", exitError.ExitCode()) + } +} + +// start a child process without the user code explicitly starting +// with a copy of the parent's SYSTEMROOT. +// (See issue 25210.) +func TestChildCriticalEnv(t *testing.T) { + t.Parallel() + cmd := helperCommand(t, "echoenv", "SYSTEMROOT") + + // Explicitly remove SYSTEMROOT from the command's environment. + var env []string + for _, kv := range cmd.Environ() { + k, _, ok := strings.Cut(kv, "=") + if !ok || !strings.EqualFold(k, "SYSTEMROOT") { + env = append(env, kv) + } + } + cmd.Env = env + + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(string(out)) == "" { + t.Error("no SYSTEMROOT found") + } +} diff --git a/src/os/exec/internal/fdtest/exists_plan9.go b/src/os/exec/internal/fdtest/exists_plan9.go new file mode 100644 index 0000000..8886e06 --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_plan9.go @@ -0,0 +1,20 @@ +// Copyright 2021 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. + +//go:build plan9 + +package fdtest + +import ( + "syscall" +) + +const errBadFd = syscall.ErrorString("fd out of range or not open") + +// Exists returns true if fd is a valid file descriptor. +func Exists(fd uintptr) bool { + var buf [1]byte + _, err := syscall.Fstat(int(fd), buf[:]) + return err != errBadFd +} diff --git a/src/os/exec/internal/fdtest/exists_test.go b/src/os/exec/internal/fdtest/exists_test.go new file mode 100644 index 0000000..a02dddf --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_test.go @@ -0,0 +1,21 @@ +// Copyright 2021 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 fdtest + +import ( + "os" + "runtime" + "testing" +) + +func TestExists(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Exists not implemented for windows") + } + + if !Exists(os.Stdout.Fd()) { + t.Errorf("Exists(%d) got false want true", os.Stdout.Fd()) + } +} diff --git a/src/os/exec/internal/fdtest/exists_unix.go b/src/os/exec/internal/fdtest/exists_unix.go new file mode 100644 index 0000000..472a802 --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_unix.go @@ -0,0 +1,19 @@ +// Copyright 2021 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. + +//go:build unix || wasm + +// Package fdtest provides test helpers for working with file descriptors across exec. +package fdtest + +import ( + "syscall" +) + +// Exists returns true if fd is a valid file descriptor. +func Exists(fd uintptr) bool { + var s syscall.Stat_t + err := syscall.Fstat(int(fd), &s) + return err != syscall.EBADF +} diff --git a/src/os/exec/internal/fdtest/exists_windows.go b/src/os/exec/internal/fdtest/exists_windows.go new file mode 100644 index 0000000..72b8ccf --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_windows.go @@ -0,0 +1,12 @@ +// Copyright 2021 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. + +//go:build windows + +package fdtest + +// Exists is not implemented on windows and panics. +func Exists(fd uintptr) bool { + panic("unimplemented") +} diff --git a/src/os/exec/internal_test.go b/src/os/exec/internal_test.go new file mode 100644 index 0000000..68d517f --- /dev/null +++ b/src/os/exec/internal_test.go @@ -0,0 +1,61 @@ +// Copyright 2015 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 exec + +import ( + "io" + "testing" +) + +func TestPrefixSuffixSaver(t *testing.T) { + tests := []struct { + N int + writes []string + want string + }{ + { + N: 2, + writes: nil, + want: "", + }, + { + N: 2, + writes: []string{"a"}, + want: "a", + }, + { + N: 2, + writes: []string{"abc", "d"}, + want: "abcd", + }, + { + N: 2, + writes: []string{"abc", "d", "e"}, + want: "ab\n... omitting 1 bytes ...\nde", + }, + { + N: 2, + writes: []string{"ab______________________yz"}, + want: "ab\n... omitting 22 bytes ...\nyz", + }, + { + N: 2, + writes: []string{"ab_______________________y", "z"}, + want: "ab\n... omitting 23 bytes ...\nyz", + }, + } + for i, tt := range tests { + w := &prefixSuffixSaver{N: tt.N} + for _, s := range tt.writes { + n, err := io.WriteString(w, s) + if err != nil || n != len(s) { + t.Errorf("%d. WriteString(%q) = %v, %v; want %v, %v", i, s, n, err, len(s), nil) + } + } + if got := string(w.Bytes()); got != tt.want { + t.Errorf("%d. Bytes = %q; want %q", i, got, tt.want) + } + } +} diff --git a/src/os/exec/lp_linux_test.go b/src/os/exec/lp_linux_test.go new file mode 100644 index 0000000..60cb13e --- /dev/null +++ b/src/os/exec/lp_linux_test.go @@ -0,0 +1,88 @@ +// Copyright 2022 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 exec_test + +import ( + "errors" + "internal/syscall/unix" + "internal/testenv" + "os" + "os/exec" + "path/filepath" + "syscall" + "testing" +) + +func TestFindExecutableVsNoexec(t *testing.T) { + t.Parallel() + + // This test case relies on faccessat2(2) syscall, which appeared in Linux v5.8. + if major, minor := unix.KernelVersion(); major < 5 || (major == 5 && minor < 8) { + t.Skip("requires Linux kernel v5.8 with faccessat2(2) syscall") + } + + tmp := t.TempDir() + + // Create a tmpfs mount. + err := syscall.Mount("tmpfs", tmp, "tmpfs", 0, "") + if testenv.SyscallIsNotSupported(err) { + // Usually this means lack of CAP_SYS_ADMIN, but there might be + // other reasons, especially in restricted test environments. + t.Skipf("requires ability to mount tmpfs (%v)", err) + } else if err != nil { + t.Fatalf("mount %s failed: %v", tmp, err) + } + t.Cleanup(func() { + if err := syscall.Unmount(tmp, 0); err != nil { + t.Error(err) + } + }) + + // Create an executable. + path := filepath.Join(tmp, "program") + err = os.WriteFile(path, []byte("#!/bin/sh\necho 123\n"), 0o755) + if err != nil { + t.Fatal(err) + } + + // Check that it works as expected. + _, err = exec.LookPath(path) + if err != nil { + t.Fatalf("findExecutable: got %v, want nil", err) + } + + for { + err = exec.Command(path).Run() + if err == nil { + break + } + if errors.Is(err, syscall.ETXTBSY) { + // A fork+exec in another process may be holding open the FD that we used + // to write the executable (see https://go.dev/issue/22315). + // Since the descriptor should have CLOEXEC set, the problem should resolve + // as soon as the forked child reaches its exec call. + // Keep retrying until that happens. + } else { + t.Fatalf("exec: got %v, want nil", err) + } + } + + // Remount with noexec flag. + err = syscall.Mount("", tmp, "", syscall.MS_REMOUNT|syscall.MS_NOEXEC, "") + if testenv.SyscallIsNotSupported(err) { + t.Skipf("requires ability to re-mount tmpfs (%v)", err) + } else if err != nil { + t.Fatalf("remount %s with noexec failed: %v", tmp, err) + } + + if err := exec.Command(path).Run(); err == nil { + t.Fatal("exec on noexec filesystem: got nil, want error") + } + + _, err = exec.LookPath(path) + if err == nil { + t.Fatalf("LookPath: got nil, want error") + } +} diff --git a/src/os/exec/lp_plan9.go b/src/os/exec/lp_plan9.go new file mode 100644 index 0000000..9344b14 --- /dev/null +++ b/src/os/exec/lp_plan9.go @@ -0,0 +1,66 @@ +// Copyright 2011 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 exec + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "strings" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in $path") + +func findExecutable(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + if m := d.Mode(); !m.IsDir() && m&0111 != 0 { + return nil + } + return fs.ErrPermission +} + +// LookPath searches for an executable named file in the +// directories named by the path environment variable. +// If file begins with "/", "#", "./", or "../", it is tried +// directly and the path is not consulted. +// On success, the result is an absolute path. +// +// In older versions of Go, LookPath could return a path relative to the current directory. +// As of Go 1.19, LookPath will instead return that path along with an error satisfying +// errors.Is(err, ErrDot). See the package documentation for more details. +func LookPath(file string) (string, error) { + // skip the path lookup for these prefixes + skip := []string{"/", "#", "./", "../"} + + for _, p := range skip { + if strings.HasPrefix(file, p) { + err := findExecutable(file) + if err == nil { + return file, nil + } + return "", &Error{file, err} + } + } + + path := os.Getenv("path") + for _, dir := range filepath.SplitList(path) { + path := filepath.Join(dir, file) + if err := findExecutable(path); err == nil { + if !filepath.IsAbs(path) { + if execerrdot.Value() != "0" { + return path, &Error{file, ErrDot} + } + execerrdot.IncNonDefault() + } + return path, nil + } + } + return "", &Error{file, ErrNotFound} +} diff --git a/src/os/exec/lp_test.go b/src/os/exec/lp_test.go new file mode 100644 index 0000000..77d8e84 --- /dev/null +++ b/src/os/exec/lp_test.go @@ -0,0 +1,33 @@ +// Copyright 2011 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 exec + +import ( + "testing" +) + +var nonExistentPaths = []string{ + "some-non-existent-path", + "non-existent-path/slashed", +} + +func TestLookPathNotFound(t *testing.T) { + for _, name := range nonExistentPaths { + path, err := LookPath(name) + if err == nil { + t.Fatalf("LookPath found %q in $PATH", name) + } + if path != "" { + t.Fatalf("LookPath path == %q when err != nil", path) + } + perr, ok := err.(*Error) + if !ok { + t.Fatal("LookPath error is not an exec.Error") + } + if perr.Name != name { + t.Fatalf("want Error name %q, got %q", name, perr.Name) + } + } +} diff --git a/src/os/exec/lp_unix.go b/src/os/exec/lp_unix.go new file mode 100644 index 0000000..fd2c6ef --- /dev/null +++ b/src/os/exec/lp_unix.go @@ -0,0 +1,82 @@ +// Copyright 2010 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. + +//go:build unix + +package exec + +import ( + "errors" + "internal/syscall/unix" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in $PATH") + +func findExecutable(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + m := d.Mode() + if m.IsDir() { + return syscall.EISDIR + } + err = unix.Eaccess(file, unix.X_OK) + // ENOSYS means Eaccess is not available or not implemented. + // EPERM can be returned by Linux containers employing seccomp. + // In both cases, fall back to checking the permission bits. + if err == nil || (err != syscall.ENOSYS && err != syscall.EPERM) { + return err + } + if m&0111 != 0 { + return nil + } + return fs.ErrPermission +} + +// LookPath searches for an executable named file in the +// directories named by the PATH environment variable. +// If file contains a slash, it is tried directly and the PATH is not consulted. +// Otherwise, on success, the result is an absolute path. +// +// In older versions of Go, LookPath could return a path relative to the current directory. +// As of Go 1.19, LookPath will instead return that path along with an error satisfying +// errors.Is(err, ErrDot). See the package documentation for more details. +func LookPath(file string) (string, error) { + // NOTE(rsc): I wish we could use the Plan 9 behavior here + // (only bypass the path if file begins with / or ./ or ../) + // but that would not match all the Unix shells. + + if strings.Contains(file, "/") { + err := findExecutable(file) + if err == nil { + return file, nil + } + return "", &Error{file, err} + } + path := os.Getenv("PATH") + for _, dir := range filepath.SplitList(path) { + if dir == "" { + // Unix shell semantics: path element "" means "." + dir = "." + } + path := filepath.Join(dir, file) + if err := findExecutable(path); err == nil { + if !filepath.IsAbs(path) { + if execerrdot.Value() != "0" { + return path, &Error{file, ErrDot} + } + execerrdot.IncNonDefault() + } + return path, nil + } + } + return "", &Error{file, ErrNotFound} +} diff --git a/src/os/exec/lp_unix_test.go b/src/os/exec/lp_unix_test.go new file mode 100644 index 0000000..181b1f0 --- /dev/null +++ b/src/os/exec/lp_unix_test.go @@ -0,0 +1,50 @@ +// Copyright 2013 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. + +//go:build unix + +package exec + +import ( + "os" + "testing" +) + +func TestLookPathUnixEmptyPath(t *testing.T) { + // Not parallel: uses os.Chdir. + + tmp, err := os.MkdirTemp("", "TestLookPathUnixEmptyPath") + if err != nil { + t.Fatal("TempDir failed: ", err) + } + defer os.RemoveAll(tmp) + wd, err := os.Getwd() + if err != nil { + t.Fatal("Getwd failed: ", err) + } + err = os.Chdir(tmp) + if err != nil { + t.Fatal("Chdir failed: ", err) + } + defer os.Chdir(wd) + + f, err := os.OpenFile("exec_me", os.O_CREATE|os.O_EXCL, 0700) + if err != nil { + t.Fatal("OpenFile failed: ", err) + } + err = f.Close() + if err != nil { + t.Fatal("Close failed: ", err) + } + + t.Setenv("PATH", "") + + path, err := LookPath("exec_me") + if err == nil { + t.Fatal("LookPath found exec_me in empty $PATH") + } + if path != "" { + t.Fatalf("LookPath path == %q when err != nil", path) + } +} diff --git a/src/os/exec/lp_wasm.go b/src/os/exec/lp_wasm.go new file mode 100644 index 0000000..f2c8e9c --- /dev/null +++ b/src/os/exec/lp_wasm.go @@ -0,0 +1,23 @@ +// Copyright 2018 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. + +//go:build wasm + +package exec + +import ( + "errors" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in $PATH") + +// LookPath searches for an executable named file in the +// directories named by the PATH environment variable. +// If file contains a slash, it is tried directly and the PATH is not consulted. +// The result may be an absolute path or a path relative to the current directory. +func LookPath(file string) (string, error) { + // Wasm can not execute processes, so act as if there are no executables at all. + return "", &Error{file, ErrNotFound} +} diff --git a/src/os/exec/lp_windows.go b/src/os/exec/lp_windows.go new file mode 100644 index 0000000..066d38d --- /dev/null +++ b/src/os/exec/lp_windows.go @@ -0,0 +1,145 @@ +// Copyright 2010 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 exec + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in %PATH%") + +func chkStat(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + if d.IsDir() { + return fs.ErrPermission + } + return nil +} + +func hasExt(file string) bool { + i := strings.LastIndex(file, ".") + if i < 0 { + return false + } + return strings.LastIndexAny(file, `:\/`) < i +} + +func findExecutable(file string, exts []string) (string, error) { + if len(exts) == 0 { + return file, chkStat(file) + } + if hasExt(file) { + if chkStat(file) == nil { + return file, nil + } + } + for _, e := range exts { + if f := file + e; chkStat(f) == nil { + return f, nil + } + } + return "", fs.ErrNotExist +} + +// LookPath searches for an executable named file in the +// directories named by the PATH environment variable. +// LookPath also uses PATHEXT environment variable to match +// a suitable candidate. +// If file contains a slash, it is tried directly and the PATH is not consulted. +// Otherwise, on success, the result is an absolute path. +// +// In older versions of Go, LookPath could return a path relative to the current directory. +// As of Go 1.19, LookPath will instead return that path along with an error satisfying +// errors.Is(err, ErrDot). See the package documentation for more details. +func LookPath(file string) (string, error) { + var exts []string + x := os.Getenv(`PATHEXT`) + if x != "" { + for _, e := range strings.Split(strings.ToLower(x), `;`) { + if e == "" { + continue + } + if e[0] != '.' { + e = "." + e + } + exts = append(exts, e) + } + } else { + exts = []string{".com", ".exe", ".bat", ".cmd"} + } + + if strings.ContainsAny(file, `:\/`) { + f, err := findExecutable(file, exts) + if err == nil { + return f, nil + } + return "", &Error{file, err} + } + + // On Windows, creating the NoDefaultCurrentDirectoryInExePath + // environment variable (with any value or no value!) signals that + // path lookups should skip the current directory. + // In theory we are supposed to call NeedCurrentDirectoryForExePathW + // "as the registry location of this environment variable can change" + // but that seems exceedingly unlikely: it would break all users who + // have configured their environment this way! + // https://docs.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-needcurrentdirectoryforexepathw + // See also go.dev/issue/43947. + var ( + dotf string + dotErr error + ) + if _, found := syscall.Getenv("NoDefaultCurrentDirectoryInExePath"); !found { + if f, err := findExecutable(filepath.Join(".", file), exts); err == nil { + if execerrdot.Value() == "0" { + execerrdot.IncNonDefault() + return f, nil + } + dotf, dotErr = f, &Error{file, ErrDot} + } + } + + path := os.Getenv("path") + for _, dir := range filepath.SplitList(path) { + if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil { + if dotErr != nil { + // https://go.dev/issue/53536: if we resolved a relative path implicitly, + // and it is the same executable that would be resolved from the explicit %PATH%, + // prefer the explicit name for the executable (and, likely, no error) instead + // of the equivalent implicit name with ErrDot. + // + // Otherwise, return the ErrDot for the implicit path as soon as we find + // out that the explicit one doesn't match. + dotfi, dotfiErr := os.Lstat(dotf) + fi, fiErr := os.Lstat(f) + if dotfiErr != nil || fiErr != nil || !os.SameFile(dotfi, fi) { + return dotf, dotErr + } + } + + if !filepath.IsAbs(f) { + if execerrdot.Value() != "0" { + return f, &Error{file, ErrDot} + } + execerrdot.IncNonDefault() + } + return f, nil + } + } + + if dotErr != nil { + return dotf, dotErr + } + return "", &Error{file, ErrNotFound} +} diff --git a/src/os/exec/lp_windows_test.go b/src/os/exec/lp_windows_test.go new file mode 100644 index 0000000..4d85a5f --- /dev/null +++ b/src/os/exec/lp_windows_test.go @@ -0,0 +1,612 @@ +// Copyright 2013 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. + +// Use an external test to avoid os/exec -> internal/testenv -> os/exec +// circular dependency. + +package exec_test + +import ( + "errors" + "fmt" + "internal/testenv" + "io" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" +) + +func init() { + registerHelperCommand("exec", cmdExec) + registerHelperCommand("lookpath", cmdLookPath) +} + +func cmdLookPath(args ...string) { + p, err := exec.LookPath(args[0]) + if err != nil { + fmt.Fprintf(os.Stderr, "LookPath failed: %v\n", err) + os.Exit(1) + } + fmt.Print(p) +} + +func cmdExec(args ...string) { + cmd := exec.Command(args[1]) + cmd.Dir = args[0] + if errors.Is(cmd.Err, exec.ErrDot) { + cmd.Err = nil + } + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Fprintf(os.Stderr, "Child: %s %s", err, string(output)) + os.Exit(1) + } + fmt.Printf("%s", string(output)) +} + +func installExe(t *testing.T, dest, src string) { + fsrc, err := os.Open(src) + if err != nil { + t.Fatal("os.Open failed: ", err) + } + defer fsrc.Close() + fdest, err := os.Create(dest) + if err != nil { + t.Fatal("os.Create failed: ", err) + } + defer fdest.Close() + _, err = io.Copy(fdest, fsrc) + if err != nil { + t.Fatal("io.Copy failed: ", err) + } +} + +func installBat(t *testing.T, dest string) { + f, err := os.Create(dest) + if err != nil { + t.Fatalf("failed to create batch file: %v", err) + } + defer f.Close() + fmt.Fprintf(f, "@echo %s\n", dest) +} + +func installProg(t *testing.T, dest, srcExe string) { + err := os.MkdirAll(filepath.Dir(dest), 0700) + if err != nil { + t.Fatal("os.MkdirAll failed: ", err) + } + if strings.ToLower(filepath.Ext(dest)) == ".bat" { + installBat(t, dest) + return + } + installExe(t, dest, srcExe) +} + +type lookPathTest struct { + rootDir string + PATH string + PATHEXT string + files []string + searchFor string + fails bool // test is expected to fail +} + +func (test lookPathTest) runProg(t *testing.T, env []string, cmd *exec.Cmd) (string, error) { + cmd.Env = env + cmd.Dir = test.rootDir + args := append([]string(nil), cmd.Args...) + args[0] = filepath.Base(args[0]) + cmdText := fmt.Sprintf("%q command", strings.Join(args, " ")) + out, err := cmd.CombinedOutput() + if (err != nil) != test.fails { + if test.fails { + t.Fatalf("test=%+v: %s succeeded, but expected to fail", test, cmdText) + } + t.Fatalf("test=%+v: %s failed, but expected to succeed: %v - %v", test, cmdText, err, string(out)) + } + if err != nil { + return "", fmt.Errorf("test=%+v: %s failed: %v - %v", test, cmdText, err, string(out)) + } + // normalise program output + p := string(out) + // trim terminating \r and \n that batch file outputs + for len(p) > 0 && (p[len(p)-1] == '\n' || p[len(p)-1] == '\r') { + p = p[:len(p)-1] + } + if !filepath.IsAbs(p) { + return p, nil + } + if p[:len(test.rootDir)] != test.rootDir { + t.Fatalf("test=%+v: %s output is wrong: %q must have %q prefix", test, cmdText, p, test.rootDir) + } + return p[len(test.rootDir)+1:], nil +} + +func updateEnv(env []string, name, value string) []string { + for i, e := range env { + if strings.HasPrefix(strings.ToUpper(e), name+"=") { + env[i] = name + "=" + value + return env + } + } + return append(env, name+"="+value) +} + +func createEnv(dir, PATH, PATHEXT string) []string { + env := os.Environ() + env = updateEnv(env, "PATHEXT", PATHEXT) + // Add dir in front of every directory in the PATH. + dirs := filepath.SplitList(PATH) + for i := range dirs { + dirs[i] = filepath.Join(dir, dirs[i]) + } + path := strings.Join(dirs, ";") + env = updateEnv(env, "PATH", os.Getenv("SystemRoot")+"/System32;"+path) + return env +} + +// createFiles copies srcPath file into multiply files. +// It uses dir as prefix for all destination files. +func createFiles(t *testing.T, dir string, files []string, srcPath string) { + for _, f := range files { + installProg(t, filepath.Join(dir, f), srcPath) + } +} + +func (test lookPathTest) run(t *testing.T, tmpdir, printpathExe string) { + test.rootDir = tmpdir + createFiles(t, test.rootDir, test.files, printpathExe) + env := createEnv(test.rootDir, test.PATH, test.PATHEXT) + // Run "cmd.exe /c test.searchFor" with new environment and + // work directory set. All candidates are copies of printpath.exe. + // These will output their program paths when run. + should, errCmd := test.runProg(t, env, testenv.Command(t, "cmd", "/c", test.searchFor)) + // Run the lookpath program with new environment and work directory set. + have, errLP := test.runProg(t, env, helperCommand(t, "lookpath", test.searchFor)) + // Compare results. + if errCmd == nil && errLP == nil { + // both succeeded + if should != have { + t.Fatalf("test=%+v:\ncmd /c ran: %s\nlookpath found: %s", test, should, have) + } + return + } + if errCmd != nil && errLP != nil { + // both failed -> continue + return + } + if errCmd != nil { + t.Fatal(errCmd) + } + if errLP != nil { + t.Fatal(errLP) + } +} + +var lookPathTests = []lookPathTest{ + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`, `p2\a`}, + searchFor: `a`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1.dir;p2.dir`, + files: []string{`p1.dir\a`, `p2.dir\a.exe`}, + searchFor: `a`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a.exe`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\b.exe`}, + searchFor: `b`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\b`, `p2\a`}, + searchFor: `a`, + fails: true, // TODO(brainman): do not know why this fails + }, + // If the command name specifies a path, the shell searches + // the specified path for an executable file matching + // the command name. If a match is found, the external + // command (the executable file) executes. + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `p2\a`, + }, + // If the command name specifies a path, the shell searches + // the specified path for an executable file matching the command + // name. ... If no match is found, the shell reports an error + // and command processing completes. + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\b.exe`, `p2\a.exe`}, + searchFor: `p2\b`, + fails: true, + }, + // If the command name does not specify a path, the shell + // searches the current directory for an executable file + // matching the command name. If a match is found, the external + // command (the executable file) executes. + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`a`, `p1\a.exe`, `p2\a.exe`}, + searchFor: `a`, + }, + // The shell now searches each directory specified by the + // PATH environment variable, in the order listed, for an + // executable file matching the command name. If a match + // is found, the external command (the executable file) executes. + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a`, + }, + // The shell now searches each directory specified by the + // PATH environment variable, in the order listed, for an + // executable file matching the command name. If no match + // is found, the shell reports an error and command processing + // completes. + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `b`, + fails: true, + }, + // If the command name includes a file extension, the shell + // searches each directory for the exact file name specified + // by the command name. + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a.exe`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a.com`, + fails: true, // includes extension and not exact file name match + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1`, + files: []string{`p1\a.exe.exe`}, + searchFor: `a.exe`, + }, + { + PATHEXT: `.COM;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a.exe`, + }, + // If the command name does not include a file extension, the shell + // adds the extensions listed in the PATHEXT environment variable, + // one by one, and searches the directory for that file name. Note + // that the shell tries all possible file extensions in a specific + // directory before moving on to search the next directory + // (if there is one). + { + PATHEXT: `.COM;.EXE`, + PATH: `p1;p2`, + files: []string{`p1\a.bat`, `p2\a.exe`}, + searchFor: `a`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.bat`, `p2\a.exe`}, + searchFor: `a`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.bat`, `p1\a.exe`, `p2\a.bat`, `p2\a.exe`}, + searchFor: `a`, + }, + { + PATHEXT: `.COM`, + PATH: `p1;p2`, + files: []string{`p1\a.bat`, `p2\a.exe`}, + searchFor: `a`, + fails: true, // tried all extensions in PATHEXT, but none matches + }, +} + +func TestLookPathWindows(t *testing.T) { + if testing.Short() { + maySkipHelperCommand("lookpath") + t.Skipf("skipping test in short mode that would build a helper binary") + } + t.Parallel() + + tmp := t.TempDir() + printpathExe := buildPrintPathExe(t, tmp) + + // Run all tests. + for i, test := range lookPathTests { + i, test := i, test + t.Run(fmt.Sprint(i), func(t *testing.T) { + t.Parallel() + + dir := filepath.Join(tmp, "d"+strconv.Itoa(i)) + err := os.Mkdir(dir, 0700) + if err != nil { + t.Fatal("Mkdir failed: ", err) + } + test.run(t, dir, printpathExe) + }) + } +} + +type commandTest struct { + PATH string + files []string + dir string + arg0 string + want string + fails bool // test is expected to fail +} + +func (test commandTest) isSuccess(rootDir, output string, err error) error { + if err != nil { + return fmt.Errorf("test=%+v: exec: %v %v", test, err, output) + } + path := output + if path[:len(rootDir)] != rootDir { + return fmt.Errorf("test=%+v: %q must have %q prefix", test, path, rootDir) + } + path = path[len(rootDir)+1:] + if path != test.want { + return fmt.Errorf("test=%+v: want %q, got %q", test, test.want, path) + } + return nil +} + +func (test commandTest) runOne(t *testing.T, rootDir string, env []string, dir, arg0 string) { + cmd := helperCommand(t, "exec", dir, arg0) + cmd.Dir = rootDir + cmd.Env = env + output, err := cmd.CombinedOutput() + err = test.isSuccess(rootDir, string(output), err) + if (err != nil) != test.fails { + if test.fails { + t.Errorf("test=%+v: succeeded, but expected to fail", test) + } else { + t.Error(err) + } + } +} + +func (test commandTest) run(t *testing.T, rootDir, printpathExe string) { + createFiles(t, rootDir, test.files, printpathExe) + PATHEXT := `.COM;.EXE;.BAT` + env := createEnv(rootDir, test.PATH, PATHEXT) + test.runOne(t, rootDir, env, test.dir, test.arg0) +} + +var commandTests = []commandTest{ + // testing commands with no slash, like `a.exe` + { + // should find a.exe in current directory + files: []string{`a.exe`}, + arg0: `a.exe`, + want: `a.exe`, + }, + { + // like above, but add PATH in attempt to break the test + PATH: `p2;p`, + files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, + arg0: `a.exe`, + want: `a.exe`, + }, + { + // like above, but use "a" instead of "a.exe" for command + PATH: `p2;p`, + files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, + arg0: `a`, + want: `a.exe`, + }, + // testing commands with slash, like `.\a.exe` + { + // should find p\a.exe + files: []string{`p\a.exe`}, + arg0: `p\a.exe`, + want: `p\a.exe`, + }, + { + // like above, but adding `.` in front of executable should still be OK + files: []string{`p\a.exe`}, + arg0: `.\p\a.exe`, + want: `p\a.exe`, + }, + { + // like above, but with PATH added in attempt to break it + PATH: `p2`, + files: []string{`p\a.exe`, `p2\a.exe`}, + arg0: `p\a.exe`, + want: `p\a.exe`, + }, + { + // like above, but make sure .exe is tried even for commands with slash + PATH: `p2`, + files: []string{`p\a.exe`, `p2\a.exe`}, + arg0: `p\a`, + want: `p\a.exe`, + }, + // tests commands, like `a.exe`, with c.Dir set + { + // should not find a.exe in p, because LookPath(`a.exe`) will fail + files: []string{`p\a.exe`}, + dir: `p`, + arg0: `a.exe`, + want: `p\a.exe`, + fails: true, + }, + { + // LookPath(`a.exe`) will find `.\a.exe`, but prefixing that with + // dir `p\a.exe` will refer to a non-existent file + files: []string{`a.exe`, `p\not_important_file`}, + dir: `p`, + arg0: `a.exe`, + want: `a.exe`, + fails: true, + }, + { + // like above, but making test succeed by installing file + // in referred destination (so LookPath(`a.exe`) will still + // find `.\a.exe`, but we successfully execute `p\a.exe`) + files: []string{`a.exe`, `p\a.exe`}, + dir: `p`, + arg0: `a.exe`, + want: `p\a.exe`, + }, + { + // like above, but add PATH in attempt to break the test + PATH: `p2;p`, + files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, + dir: `p`, + arg0: `a.exe`, + want: `p\a.exe`, + }, + { + // like above, but use "a" instead of "a.exe" for command + PATH: `p2;p`, + files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, + dir: `p`, + arg0: `a`, + want: `p\a.exe`, + }, + { + // finds `a.exe` in the PATH regardless of dir set + // because LookPath returns full path in that case + PATH: `p2;p`, + files: []string{`p\a.exe`, `p2\a.exe`}, + dir: `p`, + arg0: `a.exe`, + want: `p2\a.exe`, + }, + // tests commands, like `.\a.exe`, with c.Dir set + { + // should use dir when command is path, like ".\a.exe" + files: []string{`p\a.exe`}, + dir: `p`, + arg0: `.\a.exe`, + want: `p\a.exe`, + }, + { + // like above, but with PATH added in attempt to break it + PATH: `p2`, + files: []string{`p\a.exe`, `p2\a.exe`}, + dir: `p`, + arg0: `.\a.exe`, + want: `p\a.exe`, + }, + { + // like above, but make sure .exe is tried even for commands with slash + PATH: `p2`, + files: []string{`p\a.exe`, `p2\a.exe`}, + dir: `p`, + arg0: `.\a`, + want: `p\a.exe`, + }, +} + +func TestCommand(t *testing.T) { + if testing.Short() { + maySkipHelperCommand("exec") + t.Skipf("skipping test in short mode that would build a helper binary") + } + t.Parallel() + + tmp := t.TempDir() + printpathExe := buildPrintPathExe(t, tmp) + + // Run all tests. + for i, test := range commandTests { + i, test := i, test + t.Run(fmt.Sprint(i), func(t *testing.T) { + t.Parallel() + + dir := filepath.Join(tmp, "d"+strconv.Itoa(i)) + err := os.Mkdir(dir, 0700) + if err != nil { + t.Fatal("Mkdir failed: ", err) + } + test.run(t, dir, printpathExe) + }) + } +} + +// buildPrintPathExe creates a Go program that prints its own path. +// dir is a temp directory where executable will be created. +// The function returns full path to the created program. +func buildPrintPathExe(t *testing.T, dir string) string { + const name = "printpath" + srcname := name + ".go" + err := os.WriteFile(filepath.Join(dir, srcname), []byte(printpathSrc), 0644) + if err != nil { + t.Fatalf("failed to create source: %v", err) + } + if err != nil { + t.Fatalf("failed to execute template: %v", err) + } + outname := name + ".exe" + cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", outname, srcname) + cmd.Dir = dir + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("failed to build executable: %v - %v", err, string(out)) + } + return filepath.Join(dir, outname) +} + +const printpathSrc = ` +package main + +import ( + "os" + "syscall" + "unsafe" +) + +func getMyName() (string, error) { + var sysproc = syscall.MustLoadDLL("kernel32.dll").MustFindProc("GetModuleFileNameW") + b := make([]uint16, syscall.MAX_PATH) + r, _, err := sysproc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(len(b))) + n := uint32(r) + if n == 0 { + return "", err + } + return syscall.UTF16ToString(b[0:n]), nil +} + +func main() { + path, err := getMyName() + if err != nil { + os.Stderr.Write([]byte("getMyName failed: " + err.Error() + "\n")) + os.Exit(1) + } + os.Stdout.Write([]byte(path)) +} +` diff --git a/src/os/exec/read3.go b/src/os/exec/read3.go new file mode 100644 index 0000000..8327d73 --- /dev/null +++ b/src/os/exec/read3.go @@ -0,0 +1,91 @@ +// 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. + +//go:build ignore + +// This is a test program that verifies that it can read from +// descriptor 3 and that no other descriptors are open. +// This is not done via TestHelperProcess and GO_EXEC_TEST_PID +// because we want to ensure that this program does not use cgo, +// because C libraries can open file descriptors behind our backs +// and confuse the test. See issue 25628. +package main + +import ( + "fmt" + "internal/poll" + "io" + "os" + "os/exec" + "os/exec/internal/fdtest" + "runtime" + "strings" +) + +func main() { + fd3 := os.NewFile(3, "fd3") + defer fd3.Close() + + bs, err := io.ReadAll(fd3) + if err != nil { + fmt.Printf("ReadAll from fd 3: %v\n", err) + os.Exit(1) + } + + // Now verify that there are no other open fds. + // stdin == 0 + // stdout == 1 + // stderr == 2 + // descriptor from parent == 3 + // All descriptors 4 and up should be available, + // except for any used by the network poller. + for fd := uintptr(4); fd <= 100; fd++ { + if poll.IsPollDescriptor(fd) { + continue + } + + if !fdtest.Exists(fd) { + continue + } + + fmt.Printf("leaked parent file. fdtest.Exists(%d) got true want false\n", fd) + + fdfile := fmt.Sprintf("/proc/self/fd/%d", fd) + link, err := os.Readlink(fdfile) + fmt.Printf("readlink(%q) = %q, %v\n", fdfile, link, err) + + var args []string + switch runtime.GOOS { + case "plan9": + args = []string{fmt.Sprintf("/proc/%d/fd", os.Getpid())} + case "aix", "solaris", "illumos": + args = []string{fmt.Sprint(os.Getpid())} + default: + args = []string{"-p", fmt.Sprint(os.Getpid())} + } + + // Determine which command to use to display open files. + ofcmd := "lsof" + switch runtime.GOOS { + case "dragonfly", "freebsd", "netbsd", "openbsd": + ofcmd = "fstat" + case "plan9": + ofcmd = "/bin/cat" + case "aix": + ofcmd = "procfiles" + case "solaris", "illumos": + ofcmd = "pfiles" + } + + cmd := exec.Command(ofcmd, args...) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Fprintf(os.Stderr, "%s failed: %v\n", strings.Join(cmd.Args, " "), err) + } + fmt.Printf("%s", out) + os.Exit(1) + } + + os.Stdout.Write(bs) +} diff --git a/src/os/exec_plan9.go b/src/os/exec_plan9.go new file mode 100644 index 0000000..69714ff --- /dev/null +++ b/src/os/exec_plan9.go @@ -0,0 +1,149 @@ +// Copyright 2009 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 os + +import ( + "internal/itoa" + "runtime" + "syscall" + "time" +) + +// The only signal values guaranteed to be present in the os package +// on all systems are Interrupt (send the process an interrupt) and +// Kill (force the process to exit). Interrupt is not implemented on +// Windows; using it with os.Process.Signal will return an error. +var ( + Interrupt Signal = syscall.Note("interrupt") + Kill Signal = syscall.Note("kill") +) + +func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) { + sysattr := &syscall.ProcAttr{ + Dir: attr.Dir, + Env: attr.Env, + Sys: attr.Sys, + } + + sysattr.Files = make([]uintptr, 0, len(attr.Files)) + for _, f := range attr.Files { + sysattr.Files = append(sysattr.Files, f.Fd()) + } + + pid, h, e := syscall.StartProcess(name, argv, sysattr) + if e != nil { + return nil, &PathError{Op: "fork/exec", Path: name, Err: e} + } + + return newProcess(pid, h), nil +} + +func (p *Process) writeProcFile(file string, data string) error { + f, e := OpenFile("/proc/"+itoa.Itoa(p.Pid)+"/"+file, O_WRONLY, 0) + if e != nil { + return e + } + defer f.Close() + _, e = f.Write([]byte(data)) + return e +} + +func (p *Process) signal(sig Signal) error { + if p.done() { + return ErrProcessDone + } + if e := p.writeProcFile("note", sig.String()); e != nil { + return NewSyscallError("signal", e) + } + return nil +} + +func (p *Process) kill() error { + return p.signal(Kill) +} + +func (p *Process) wait() (ps *ProcessState, err error) { + var waitmsg syscall.Waitmsg + + if p.Pid == -1 { + return nil, ErrInvalid + } + err = syscall.WaitProcess(p.Pid, &waitmsg) + if err != nil { + return nil, NewSyscallError("wait", err) + } + + p.setDone() + ps = &ProcessState{ + pid: waitmsg.Pid, + status: &waitmsg, + } + return ps, nil +} + +func (p *Process) release() error { + // NOOP for Plan 9. + p.Pid = -1 + // no need for a finalizer anymore + runtime.SetFinalizer(p, nil) + return nil +} + +func findProcess(pid int) (p *Process, err error) { + // NOOP for Plan 9. + return newProcess(pid, 0), nil +} + +// ProcessState stores information about a process, as reported by Wait. +type ProcessState struct { + pid int // The process's id. + status *syscall.Waitmsg // System-dependent status info. +} + +// Pid returns the process id of the exited process. +func (p *ProcessState) Pid() int { + return p.pid +} + +func (p *ProcessState) exited() bool { + return p.status.Exited() +} + +func (p *ProcessState) success() bool { + return p.status.ExitStatus() == 0 +} + +func (p *ProcessState) sys() any { + return p.status +} + +func (p *ProcessState) sysUsage() any { + return p.status +} + +func (p *ProcessState) userTime() time.Duration { + return time.Duration(p.status.Time[0]) * time.Millisecond +} + +func (p *ProcessState) systemTime() time.Duration { + return time.Duration(p.status.Time[1]) * time.Millisecond +} + +func (p *ProcessState) String() string { + if p == nil { + return "" + } + return "exit status: " + p.status.Msg +} + +// ExitCode returns the exit code of the exited process, or -1 +// if the process hasn't exited or was terminated by a signal. +func (p *ProcessState) ExitCode() int { + // return -1 if the process hasn't started. + if p == nil { + return -1 + } + return p.status.ExitStatus() +} diff --git a/src/os/exec_posix.go b/src/os/exec_posix.go new file mode 100644 index 0000000..a512d51 --- /dev/null +++ b/src/os/exec_posix.go @@ -0,0 +1,136 @@ +// Copyright 2009 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. + +//go:build unix || (js && wasm) || wasip1 || windows + +package os + +import ( + "internal/itoa" + "internal/syscall/execenv" + "runtime" + "syscall" +) + +// The only signal values guaranteed to be present in the os package on all +// systems are os.Interrupt (send the process an interrupt) and os.Kill (force +// the process to exit). On Windows, sending os.Interrupt to a process with +// os.Process.Signal is not implemented; it will return an error instead of +// sending a signal. +var ( + Interrupt Signal = syscall.SIGINT + Kill Signal = syscall.SIGKILL +) + +func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) { + // If there is no SysProcAttr (ie. no Chroot or changed + // UID/GID), double-check existence of the directory we want + // to chdir into. We can make the error clearer this way. + if attr != nil && attr.Sys == nil && attr.Dir != "" { + if _, err := Stat(attr.Dir); err != nil { + pe := err.(*PathError) + pe.Op = "chdir" + return nil, pe + } + } + + sysattr := &syscall.ProcAttr{ + Dir: attr.Dir, + Env: attr.Env, + Sys: attr.Sys, + } + if sysattr.Env == nil { + sysattr.Env, err = execenv.Default(sysattr.Sys) + if err != nil { + return nil, err + } + } + sysattr.Files = make([]uintptr, 0, len(attr.Files)) + for _, f := range attr.Files { + sysattr.Files = append(sysattr.Files, f.Fd()) + } + + pid, h, e := syscall.StartProcess(name, argv, sysattr) + + // Make sure we don't run the finalizers of attr.Files. + runtime.KeepAlive(attr) + + if e != nil { + return nil, &PathError{Op: "fork/exec", Path: name, Err: e} + } + + return newProcess(pid, h), nil +} + +func (p *Process) kill() error { + return p.Signal(Kill) +} + +// ProcessState stores information about a process, as reported by Wait. +type ProcessState struct { + pid int // The process's id. + status syscall.WaitStatus // System-dependent status info. + rusage *syscall.Rusage +} + +// Pid returns the process id of the exited process. +func (p *ProcessState) Pid() int { + return p.pid +} + +func (p *ProcessState) exited() bool { + return p.status.Exited() +} + +func (p *ProcessState) success() bool { + return p.status.ExitStatus() == 0 +} + +func (p *ProcessState) sys() any { + return p.status +} + +func (p *ProcessState) sysUsage() any { + return p.rusage +} + +func (p *ProcessState) String() string { + if p == nil { + return "" + } + status := p.Sys().(syscall.WaitStatus) + res := "" + switch { + case status.Exited(): + code := status.ExitStatus() + if runtime.GOOS == "windows" && uint(code) >= 1<<16 { // windows uses large hex numbers + res = "exit status " + uitox(uint(code)) + } else { // unix systems use small decimal integers + res = "exit status " + itoa.Itoa(code) // unix + } + case status.Signaled(): + res = "signal: " + status.Signal().String() + case status.Stopped(): + res = "stop signal: " + status.StopSignal().String() + if status.StopSignal() == syscall.SIGTRAP && status.TrapCause() != 0 { + res += " (trap " + itoa.Itoa(status.TrapCause()) + ")" + } + case status.Continued(): + res = "continued" + } + if status.CoreDump() { + res += " (core dumped)" + } + return res +} + +// ExitCode returns the exit code of the exited process, or -1 +// if the process hasn't exited or was terminated by a signal. +func (p *ProcessState) ExitCode() int { + // return -1 if the process hasn't started. + if p == nil { + return -1 + } + return p.status.ExitStatus() +} diff --git a/src/os/exec_unix.go b/src/os/exec_unix.go new file mode 100644 index 0000000..f9063b4 --- /dev/null +++ b/src/os/exec_unix.go @@ -0,0 +1,106 @@ +// Copyright 2009 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. + +//go:build unix || (js && wasm) || wasip1 + +package os + +import ( + "errors" + "runtime" + "syscall" + "time" +) + +func (p *Process) wait() (ps *ProcessState, err error) { + if p.Pid == -1 { + return nil, syscall.EINVAL + } + + // If we can block until Wait4 will succeed immediately, do so. + ready, err := p.blockUntilWaitable() + if err != nil { + return nil, err + } + if ready { + // Mark the process done now, before the call to Wait4, + // so that Process.signal will not send a signal. + p.setDone() + // Acquire a write lock on sigMu to wait for any + // active call to the signal method to complete. + p.sigMu.Lock() + p.sigMu.Unlock() + } + + var ( + status syscall.WaitStatus + rusage syscall.Rusage + pid1 int + e error + ) + for { + pid1, e = syscall.Wait4(p.Pid, &status, 0, &rusage) + if e != syscall.EINTR { + break + } + } + if e != nil { + return nil, NewSyscallError("wait", e) + } + if pid1 != 0 { + p.setDone() + } + ps = &ProcessState{ + pid: pid1, + status: status, + rusage: &rusage, + } + return ps, nil +} + +func (p *Process) signal(sig Signal) error { + if p.Pid == -1 { + return errors.New("os: process already released") + } + if p.Pid == 0 { + return errors.New("os: process not initialized") + } + p.sigMu.RLock() + defer p.sigMu.RUnlock() + if p.done() { + return ErrProcessDone + } + s, ok := sig.(syscall.Signal) + if !ok { + return errors.New("os: unsupported signal type") + } + if e := syscall.Kill(p.Pid, s); e != nil { + if e == syscall.ESRCH { + return ErrProcessDone + } + return e + } + return nil +} + +func (p *Process) release() error { + // NOOP for unix. + p.Pid = -1 + // no need for a finalizer anymore + runtime.SetFinalizer(p, nil) + return nil +} + +func findProcess(pid int) (p *Process, err error) { + // NOOP for unix. + return newProcess(pid, 0), nil +} + +func (p *ProcessState) userTime() time.Duration { + return time.Duration(p.rusage.Utime.Nano()) * time.Nanosecond +} + +func (p *ProcessState) systemTime() time.Duration { + return time.Duration(p.rusage.Stime.Nano()) * time.Nanosecond +} diff --git a/src/os/exec_unix_test.go b/src/os/exec_unix_test.go new file mode 100644 index 0000000..2604519 --- /dev/null +++ b/src/os/exec_unix_test.go @@ -0,0 +1,45 @@ +// 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. + +//go:build unix + +package os_test + +import ( + "internal/testenv" + . "os" + "syscall" + "testing" +) + +func TestErrProcessDone(t *testing.T) { + testenv.MustHaveGoBuild(t) + t.Parallel() + + p, err := StartProcess(testenv.GoToolPath(t), []string{"go"}, &ProcAttr{}) + if err != nil { + t.Errorf("starting test process: %v", err) + } + p.Wait() + if got := p.Signal(Kill); got != ErrProcessDone { + t.Errorf("got %v want %v", got, ErrProcessDone) + } +} + +func TestUNIXProcessAlive(t *testing.T) { + testenv.MustHaveGoBuild(t) + t.Parallel() + + p, err := StartProcess(testenv.GoToolPath(t), []string{"sleep", "1"}, &ProcAttr{}) + if err != nil { + t.Skipf("starting test process: %v", err) + } + defer p.Kill() + + proc, _ := FindProcess(p.Pid) + err = proc.Signal(syscall.Signal(0)) + if err != nil { + t.Errorf("OS reported error for running process: %v", err) + } +} diff --git a/src/os/exec_windows.go b/src/os/exec_windows.go new file mode 100644 index 0000000..239bed1 --- /dev/null +++ b/src/os/exec_windows.go @@ -0,0 +1,181 @@ +// Copyright 2009 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 os + +import ( + "errors" + "internal/syscall/windows" + "runtime" + "sync/atomic" + "syscall" + "time" +) + +func (p *Process) wait() (ps *ProcessState, err error) { + handle := atomic.LoadUintptr(&p.handle) + s, e := syscall.WaitForSingleObject(syscall.Handle(handle), syscall.INFINITE) + switch s { + case syscall.WAIT_OBJECT_0: + break + case syscall.WAIT_FAILED: + return nil, NewSyscallError("WaitForSingleObject", e) + default: + return nil, errors.New("os: unexpected result from WaitForSingleObject") + } + var ec uint32 + e = syscall.GetExitCodeProcess(syscall.Handle(handle), &ec) + if e != nil { + return nil, NewSyscallError("GetExitCodeProcess", e) + } + var u syscall.Rusage + e = syscall.GetProcessTimes(syscall.Handle(handle), &u.CreationTime, &u.ExitTime, &u.KernelTime, &u.UserTime) + if e != nil { + return nil, NewSyscallError("GetProcessTimes", e) + } + p.setDone() + // NOTE(brainman): It seems that sometimes process is not dead + // when WaitForSingleObject returns. But we do not know any + // other way to wait for it. Sleeping for a while seems to do + // the trick sometimes. + // See https://golang.org/issue/25965 for details. + defer time.Sleep(5 * time.Millisecond) + defer p.Release() + return &ProcessState{p.Pid, syscall.WaitStatus{ExitCode: ec}, &u}, nil +} + +func (p *Process) signal(sig Signal) error { + handle := atomic.LoadUintptr(&p.handle) + if handle == uintptr(syscall.InvalidHandle) { + return syscall.EINVAL + } + if p.done() { + return ErrProcessDone + } + if sig == Kill { + var terminationHandle syscall.Handle + e := syscall.DuplicateHandle(^syscall.Handle(0), syscall.Handle(handle), ^syscall.Handle(0), &terminationHandle, syscall.PROCESS_TERMINATE, false, 0) + if e != nil { + return NewSyscallError("DuplicateHandle", e) + } + runtime.KeepAlive(p) + defer syscall.CloseHandle(terminationHandle) + e = syscall.TerminateProcess(syscall.Handle(terminationHandle), 1) + return NewSyscallError("TerminateProcess", e) + } + // TODO(rsc): Handle Interrupt too? + return syscall.Errno(syscall.EWINDOWS) +} + +func (p *Process) release() error { + handle := atomic.SwapUintptr(&p.handle, uintptr(syscall.InvalidHandle)) + if handle == uintptr(syscall.InvalidHandle) { + return syscall.EINVAL + } + e := syscall.CloseHandle(syscall.Handle(handle)) + if e != nil { + return NewSyscallError("CloseHandle", e) + } + // no need for a finalizer anymore + runtime.SetFinalizer(p, nil) + return nil +} + +func findProcess(pid int) (p *Process, err error) { + const da = syscall.STANDARD_RIGHTS_READ | + syscall.PROCESS_QUERY_INFORMATION | syscall.SYNCHRONIZE + h, e := syscall.OpenProcess(da, false, uint32(pid)) + if e != nil { + return nil, NewSyscallError("OpenProcess", e) + } + return newProcess(pid, uintptr(h)), nil +} + +func init() { + cmd := windows.UTF16PtrToString(syscall.GetCommandLine()) + if len(cmd) == 0 { + arg0, _ := Executable() + Args = []string{arg0} + } else { + Args = commandLineToArgv(cmd) + } +} + +// appendBSBytes appends n '\\' bytes to b and returns the resulting slice. +func appendBSBytes(b []byte, n int) []byte { + for ; n > 0; n-- { + b = append(b, '\\') + } + return b +} + +// readNextArg splits command line string cmd into next +// argument and command line remainder. +func readNextArg(cmd string) (arg []byte, rest string) { + var b []byte + var inquote bool + var nslash int + for ; len(cmd) > 0; cmd = cmd[1:] { + c := cmd[0] + switch c { + case ' ', '\t': + if !inquote { + return appendBSBytes(b, nslash), cmd[1:] + } + case '"': + b = appendBSBytes(b, nslash/2) + if nslash%2 == 0 { + // use "Prior to 2008" rule from + // http://daviddeley.com/autohotkey/parameters/parameters.htm + // section 5.2 to deal with double double quotes + if inquote && len(cmd) > 1 && cmd[1] == '"' { + b = append(b, c) + cmd = cmd[1:] + } + inquote = !inquote + } else { + b = append(b, c) + } + nslash = 0 + continue + case '\\': + nslash++ + continue + } + b = appendBSBytes(b, nslash) + nslash = 0 + b = append(b, c) + } + return appendBSBytes(b, nslash), "" +} + +// commandLineToArgv splits a command line into individual argument +// strings, following the Windows conventions documented +// at http://daviddeley.com/autohotkey/parameters/parameters.htm#WINARGV +func commandLineToArgv(cmd string) []string { + var args []string + for len(cmd) > 0 { + if cmd[0] == ' ' || cmd[0] == '\t' { + cmd = cmd[1:] + continue + } + var arg []byte + arg, cmd = readNextArg(cmd) + args = append(args, string(arg)) + } + return args +} + +func ftToDuration(ft *syscall.Filetime) time.Duration { + n := int64(ft.HighDateTime)<<32 + int64(ft.LowDateTime) // in 100-nanosecond intervals + return time.Duration(n*100) * time.Nanosecond +} + +func (p *ProcessState) userTime() time.Duration { + return ftToDuration(&p.rusage.UserTime) +} + +func (p *ProcessState) systemTime() time.Duration { + return ftToDuration(&p.rusage.KernelTime) +} diff --git a/src/os/executable.go b/src/os/executable.go new file mode 100644 index 0000000..cc3134a --- /dev/null +++ b/src/os/executable.go @@ -0,0 +1,20 @@ +// Copyright 2016 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 os + +// Executable returns the path name for the executable that started +// the current process. There is no guarantee that the path is still +// pointing to the correct executable. If a symlink was used to start +// the process, depending on the operating system, the result might +// be the symlink or the path it pointed to. If a stable result is +// needed, path/filepath.EvalSymlinks might help. +// +// Executable returns an absolute path unless an error occurred. +// +// The main use case is finding resources located relative to an +// executable. +func Executable() (string, error) { + return executable() +} diff --git a/src/os/executable_darwin.go b/src/os/executable_darwin.go new file mode 100644 index 0000000..dae9f4e --- /dev/null +++ b/src/os/executable_darwin.go @@ -0,0 +1,29 @@ +// Copyright 2016 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 os + +import "errors" + +var executablePath string // set by ../runtime/os_darwin.go + +var initCwd, initCwdErr = Getwd() + +func executable() (string, error) { + ep := executablePath + if len(ep) == 0 { + return ep, errors.New("cannot find executable path") + } + if ep[0] != '/' { + if initCwdErr != nil { + return ep, initCwdErr + } + if len(ep) > 2 && ep[0:2] == "./" { + // skip "./" + ep = ep[2:] + } + ep = initCwd + "/" + ep + } + return ep, nil +} diff --git a/src/os/executable_dragonfly.go b/src/os/executable_dragonfly.go new file mode 100644 index 0000000..19c2ae8 --- /dev/null +++ b/src/os/executable_dragonfly.go @@ -0,0 +1,12 @@ +// 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 os + +// From DragonFly's +const ( + _CTL_KERN = 1 + _KERN_PROC = 14 + _KERN_PROC_PATHNAME = 9 +) diff --git a/src/os/executable_freebsd.go b/src/os/executable_freebsd.go new file mode 100644 index 0000000..95f1a93 --- /dev/null +++ b/src/os/executable_freebsd.go @@ -0,0 +1,12 @@ +// 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 os + +// From FreeBSD's +const ( + _CTL_KERN = 1 + _KERN_PROC = 14 + _KERN_PROC_PATHNAME = 12 +) diff --git a/src/os/executable_path.go b/src/os/executable_path.go new file mode 100644 index 0000000..d6161bc --- /dev/null +++ b/src/os/executable_path.go @@ -0,0 +1,104 @@ +// Copyright 2017 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. + +//go:build aix || openbsd + +package os + +// We query the working directory at init, to use it later to search for the +// executable file +// errWd will be checked later, if we need to use initWd +var initWd, errWd = Getwd() + +func executable() (string, error) { + var exePath string + if len(Args) == 0 || Args[0] == "" { + return "", ErrNotExist + } + if IsPathSeparator(Args[0][0]) { + // Args[0] is an absolute path, so it is the executable. + // Note that we only need to worry about Unix paths here. + exePath = Args[0] + } else { + for i := 1; i < len(Args[0]); i++ { + if IsPathSeparator(Args[0][i]) { + // Args[0] is a relative path: prepend the + // initial working directory. + if errWd != nil { + return "", errWd + } + exePath = initWd + string(PathSeparator) + Args[0] + break + } + } + } + if exePath != "" { + if err := isExecutable(exePath); err != nil { + return "", err + } + return exePath, nil + } + // Search for executable in $PATH. + for _, dir := range splitPathList(Getenv("PATH")) { + if len(dir) == 0 { + dir = "." + } + if !IsPathSeparator(dir[0]) { + if errWd != nil { + return "", errWd + } + dir = initWd + string(PathSeparator) + dir + } + exePath = dir + string(PathSeparator) + Args[0] + switch isExecutable(exePath) { + case nil: + return exePath, nil + case ErrPermission: + return "", ErrPermission + } + } + return "", ErrNotExist +} + +// isExecutable returns an error if a given file is not an executable. +func isExecutable(path string) error { + stat, err := Stat(path) + if err != nil { + return err + } + mode := stat.Mode() + if !mode.IsRegular() { + return ErrPermission + } + if (mode & 0111) == 0 { + return ErrPermission + } + return nil +} + +// splitPathList splits a path list. +// This is based on genSplit from strings/strings.go +func splitPathList(pathList string) []string { + if pathList == "" { + return nil + } + n := 1 + for i := 0; i < len(pathList); i++ { + if pathList[i] == PathListSeparator { + n++ + } + } + start := 0 + a := make([]string, n) + na := 0 + for i := 0; i+1 <= len(pathList) && na+1 < n; i++ { + if pathList[i] == PathListSeparator { + a[na] = pathList[start:i] + na++ + start = i + 1 + } + } + a[na] = pathList[start:] + return a[:na+1] +} diff --git a/src/os/executable_plan9.go b/src/os/executable_plan9.go new file mode 100644 index 0000000..8d8c832 --- /dev/null +++ b/src/os/executable_plan9.go @@ -0,0 +1,22 @@ +// Copyright 2016 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. + +//go:build plan9 + +package os + +import ( + "internal/itoa" + "syscall" +) + +func executable() (string, error) { + fn := "/proc/" + itoa.Itoa(Getpid()) + "/text" + f, err := Open(fn) + if err != nil { + return "", err + } + defer f.Close() + return syscall.Fd2path(int(f.Fd())) +} diff --git a/src/os/executable_procfs.go b/src/os/executable_procfs.go new file mode 100644 index 0000000..94e674e --- /dev/null +++ b/src/os/executable_procfs.go @@ -0,0 +1,37 @@ +// Copyright 2016 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. + +//go:build linux || netbsd + +package os + +import ( + "errors" + "runtime" +) + +func executable() (string, error) { + var procfn string + switch runtime.GOOS { + default: + return "", errors.New("Executable not implemented for " + runtime.GOOS) + case "linux", "android": + procfn = "/proc/self/exe" + case "netbsd": + procfn = "/proc/curproc/exe" + } + path, err := Readlink(procfn) + + // When the executable has been deleted then Readlink returns a + // path appended with " (deleted)". + return stringsTrimSuffix(path, " (deleted)"), err +} + +// stringsTrimSuffix is the same as strings.TrimSuffix. +func stringsTrimSuffix(s, suffix string) string { + if len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix { + return s[:len(s)-len(suffix)] + } + return s +} diff --git a/src/os/executable_solaris.go b/src/os/executable_solaris.go new file mode 100644 index 0000000..b145980 --- /dev/null +++ b/src/os/executable_solaris.go @@ -0,0 +1,32 @@ +// Copyright 2016 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 os + +import "syscall" + +var executablePath string // set by sysauxv in ../runtime/os3_solaris.go + +var initCwd, initCwdErr = Getwd() + +func executable() (string, error) { + path := executablePath + if len(path) == 0 { + path, err := syscall.Getexecname() + if err != nil { + return path, err + } + } + if len(path) > 0 && path[0] != '/' { + if initCwdErr != nil { + return path, initCwdErr + } + if len(path) > 2 && path[0:2] == "./" { + // skip "./" + path = path[2:] + } + return initCwd + "/" + path, nil + } + return path, nil +} diff --git a/src/os/executable_sysctl.go b/src/os/executable_sysctl.go new file mode 100644 index 0000000..3c2aeac --- /dev/null +++ b/src/os/executable_sysctl.go @@ -0,0 +1,35 @@ +// Copyright 2016 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. + +//go:build freebsd || dragonfly + +package os + +import ( + "syscall" + "unsafe" +) + +func executable() (string, error) { + mib := [4]int32{_CTL_KERN, _KERN_PROC, _KERN_PROC_PATHNAME, -1} + + n := uintptr(0) + // get length + _, _, err := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0) + if err != 0 { + return "", err + } + if n == 0 { // shouldn't happen + return "", nil + } + buf := make([]byte, n) + _, _, err = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0) + if err != 0 { + return "", err + } + if n == 0 { // shouldn't happen + return "", nil + } + return string(buf[:n-1]), nil +} diff --git a/src/os/executable_test.go b/src/os/executable_test.go new file mode 100644 index 0000000..c835bb4 --- /dev/null +++ b/src/os/executable_test.go @@ -0,0 +1,155 @@ +// Copyright 2016 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 os_test + +import ( + "fmt" + "internal/testenv" + "os" + "path/filepath" + "runtime" + "testing" +) + +const executable_EnvVar = "OSTEST_OUTPUT_EXECPATH" + +func TestExecutable(t *testing.T) { + testenv.MustHaveExec(t) + t.Parallel() + + ep, err := os.Executable() + if err != nil { + t.Fatalf("Executable failed: %v", err) + } + // we want fn to be of the form "dir/prog" + dir := filepath.Dir(filepath.Dir(ep)) + fn, err := filepath.Rel(dir, ep) + if err != nil { + t.Fatalf("filepath.Rel: %v", err) + } + + cmd := testenv.Command(t, fn, "-test.run=XXXX") + // make child start with a relative program path + cmd.Dir = dir + cmd.Path = fn + if runtime.GOOS == "openbsd" || runtime.GOOS == "aix" { + // OpenBSD and AIX rely on argv[0] + } else { + // forge argv[0] for child, so that we can verify we could correctly + // get real path of the executable without influenced by argv[0]. + cmd.Args[0] = "-" + } + cmd.Env = append(cmd.Environ(), fmt.Sprintf("%s=1", executable_EnvVar)) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("exec(self) failed: %v", err) + } + outs := string(out) + if !filepath.IsAbs(outs) { + t.Fatalf("Child returned %q, want an absolute path", out) + } + if !sameFile(outs, ep) { + t.Fatalf("Child returned %q, not the same file as %q", out, ep) + } +} + +func sameFile(fn1, fn2 string) bool { + fi1, err := os.Stat(fn1) + if err != nil { + return false + } + fi2, err := os.Stat(fn2) + if err != nil { + return false + } + return os.SameFile(fi1, fi2) +} + +func init() { + if e := os.Getenv(executable_EnvVar); e != "" { + // first chdir to another path + dir := "/" + if runtime.GOOS == "windows" { + cwd, err := os.Getwd() + if err != nil { + panic(err) + } + dir = filepath.VolumeName(cwd) + } + os.Chdir(dir) + if ep, err := os.Executable(); err != nil { + fmt.Fprint(os.Stderr, "ERROR: ", err) + } else { + fmt.Fprint(os.Stderr, ep) + } + os.Exit(0) + } +} + +func TestExecutableDeleted(t *testing.T) { + testenv.MustHaveGoBuild(t) + switch runtime.GOOS { + case "windows", "plan9": + t.Skipf("%v does not support deleting running binary", runtime.GOOS) + case "openbsd", "freebsd", "aix": + t.Skipf("%v does not support reading deleted binary name", runtime.GOOS) + } + t.Parallel() + + dir := t.TempDir() + + src := filepath.Join(dir, "testdel.go") + exe := filepath.Join(dir, "testdel.exe") + + err := os.WriteFile(src, []byte(testExecutableDeletion), 0666) + if err != nil { + t.Fatal(err) + } + + out, err := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", exe, src).CombinedOutput() + t.Logf("build output:\n%s", out) + if err != nil { + t.Fatal(err) + } + + out, err = testenv.Command(t, exe).CombinedOutput() + t.Logf("exec output:\n%s", out) + if err != nil { + t.Fatal(err) + } +} + +const testExecutableDeletion = `package main + +import ( + "fmt" + "os" +) + +func main() { + before, err := os.Executable() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to read executable name before deletion: %v\n", err) + os.Exit(1) + } + + err = os.Remove(before) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to remove executable: %v\n", err) + os.Exit(1) + } + + after, err := os.Executable() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to read executable name after deletion: %v\n", err) + os.Exit(1) + } + + if before != after { + fmt.Fprintf(os.Stderr, "before and after do not match: %v != %v\n", before, after) + os.Exit(1) + } +} +` diff --git a/src/os/executable_wasm.go b/src/os/executable_wasm.go new file mode 100644 index 0000000..a88360c --- /dev/null +++ b/src/os/executable_wasm.go @@ -0,0 +1,16 @@ +// 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. + +//go:build wasm + +package os + +import ( + "errors" + "runtime" +) + +func executable() (string, error) { + return "", errors.New("Executable not implemented for " + runtime.GOOS) +} diff --git a/src/os/executable_windows.go b/src/os/executable_windows.go new file mode 100644 index 0000000..fc5cf86 --- /dev/null +++ b/src/os/executable_windows.go @@ -0,0 +1,32 @@ +// Copyright 2016 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 os + +import ( + "internal/syscall/windows" + "syscall" +) + +func getModuleFileName(handle syscall.Handle) (string, error) { + n := uint32(1024) + var buf []uint16 + for { + buf = make([]uint16, n) + r, err := windows.GetModuleFileName(handle, &buf[0], n) + if err != nil { + return "", err + } + if r < n { + break + } + // r == n means n not big enough + n += 1024 + } + return syscall.UTF16ToString(buf), nil +} + +func executable() (string, error) { + return getModuleFileName(0) +} diff --git a/src/os/export_linux_test.go b/src/os/export_linux_test.go new file mode 100644 index 0000000..3fd5e61 --- /dev/null +++ b/src/os/export_linux_test.go @@ -0,0 +1,11 @@ +// 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 os + +var ( + PollCopyFileRangeP = &pollCopyFileRange + PollSpliceFile = &pollSplice + GetPollFDForTest = getPollFD +) diff --git a/src/os/export_test.go b/src/os/export_test.go new file mode 100644 index 0000000..dc7caae --- /dev/null +++ b/src/os/export_test.go @@ -0,0 +1,17 @@ +// Copyright 2011 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 os + +// Export for testing. + +var Atime = atime +var LstatP = &lstat +var ErrWriteAtInAppendMode = errWriteAtInAppendMode +var TestingForceReadDirLstat = &testingForceReadDirLstat +var ErrPatternHasSeparator = errPatternHasSeparator + +func init() { + checkWrapErr = true +} diff --git a/src/os/export_unix_test.go b/src/os/export_unix_test.go new file mode 100644 index 0000000..b8dcca0 --- /dev/null +++ b/src/os/export_unix_test.go @@ -0,0 +1,9 @@ +// Copyright 2019 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. + +//go:build unix || (js && wasm) || wasip1 + +package os + +var SplitPath = splitPath diff --git a/src/os/export_windows_test.go b/src/os/export_windows_test.go new file mode 100644 index 0000000..ff4f899 --- /dev/null +++ b/src/os/export_windows_test.go @@ -0,0 +1,14 @@ +// Copyright 2016 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 os + +// Export for testing. + +var ( + FixLongPath = fixLongPath + CanUseLongPaths = canUseLongPaths + NewConsoleFile = newConsoleFile + CommandLineToArgv = commandLineToArgv +) diff --git a/src/os/fifo_test.go b/src/os/fifo_test.go new file mode 100644 index 0000000..df4b2ee --- /dev/null +++ b/src/os/fifo_test.go @@ -0,0 +1,207 @@ +// Copyright 2015 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. + +//go:build darwin || dragonfly || freebsd || (linux && !android) || netbsd || openbsd + +package os_test + +import ( + "errors" + "internal/syscall/unix" + "internal/testenv" + "io/fs" + "os" + "path/filepath" + "strconv" + "sync" + "syscall" + "testing" +) + +func TestFifoEOF(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + fifoName := filepath.Join(dir, "fifo") + if err := syscall.Mkfifo(fifoName, 0600); err != nil { + t.Fatal(err) + } + + // Per https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html#tag_16_357_03: + // + // - “If O_NONBLOCK is clear, an open() for reading-only shall block the + // calling thread until a thread opens the file for writing. An open() for + // writing-only shall block the calling thread until a thread opens the file + // for reading.” + // + // In order to unblock both open calls, we open the two ends of the FIFO + // simultaneously in separate goroutines. + + rc := make(chan *os.File, 1) + go func() { + r, err := os.Open(fifoName) + if err != nil { + t.Error(err) + } + rc <- r + }() + + w, err := os.OpenFile(fifoName, os.O_WRONLY, 0) + if err != nil { + t.Error(err) + } + + r := <-rc + if t.Failed() { + if r != nil { + r.Close() + } + if w != nil { + w.Close() + } + return + } + + testPipeEOF(t, r, w) +} + +// Issue #59545. +func TestNonPollable(t *testing.T) { + if testing.Short() { + t.Skip("skipping test with tight loops in short mode") + } + + // We need to open a non-pollable file. + // This is almost certainly Linux-specific, + // but if other systems have non-pollable files, + // we can add them here. + const nonPollable = "/dev/net/tun" + + f, err := os.OpenFile(nonPollable, os.O_RDWR, 0) + if err != nil { + if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) || testenv.SyscallIsNotSupported(err) { + t.Skipf("can't open %q: %v", nonPollable, err) + } + t.Fatal(err) + } + f.Close() + + // On a Linux laptop, before the problem was fixed, + // this test failed about 50% of the time with this + // number of iterations. + // It takes about 1/2 second when it passes. + const attempts = 20000 + + start := make(chan bool) + var wg sync.WaitGroup + wg.Add(1) + defer wg.Wait() + go func() { + defer wg.Done() + close(start) + for i := 0; i < attempts; i++ { + f, err := os.OpenFile(nonPollable, os.O_RDWR, 0) + if err != nil { + t.Error(err) + return + } + if err := f.Close(); err != nil { + t.Error(err) + return + } + } + }() + + dir := t.TempDir() + <-start + for i := 0; i < attempts; i++ { + name := filepath.Join(dir, strconv.Itoa(i)) + if err := syscall.Mkfifo(name, 0o600); err != nil { + t.Fatal(err) + } + // The problem only occurs if we use O_NONBLOCK here. + rd, err := os.OpenFile(name, os.O_RDONLY|syscall.O_NONBLOCK, 0o600) + if err != nil { + t.Fatal(err) + } + wr, err := os.OpenFile(name, os.O_WRONLY|syscall.O_NONBLOCK, 0o600) + if err != nil { + t.Fatal(err) + } + const msg = "message" + if _, err := wr.Write([]byte(msg)); err != nil { + if errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.ENOBUFS) { + t.Logf("ignoring write error %v", err) + rd.Close() + wr.Close() + continue + } + t.Fatalf("write to fifo %d failed: %v", i, err) + } + if _, err := rd.Read(make([]byte, len(msg))); err != nil { + if errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.ENOBUFS) { + t.Logf("ignoring read error %v", err) + rd.Close() + wr.Close() + continue + } + t.Fatalf("read from fifo %d failed; %v", i, err) + } + if err := rd.Close(); err != nil { + t.Fatal(err) + } + if err := wr.Close(); err != nil { + t.Fatal(err) + } + } +} + +// Issue 60211. +func TestOpenFileNonBlocking(t *testing.T) { + exe, err := os.Executable() + if err != nil { + t.Skipf("can't find executable: %v", err) + } + f, err := os.OpenFile(exe, os.O_RDONLY|syscall.O_NONBLOCK, 0666) + if err != nil { + t.Fatal(err) + } + defer f.Close() + nonblock, err := unix.IsNonblock(int(f.Fd())) + if err != nil { + t.Fatal(err) + } + if !nonblock { + t.Errorf("file opened with O_NONBLOCK but in blocking mode") + } +} + +func TestNewFileNonBlocking(t *testing.T) { + var p [2]int + if err := syscall.Pipe(p[:]); err != nil { + t.Fatal(err) + } + if err := syscall.SetNonblock(p[0], true); err != nil { + t.Fatal(err) + } + f := os.NewFile(uintptr(p[0]), "pipe") + nonblock, err := unix.IsNonblock(p[0]) + if err != nil { + t.Fatal(err) + } + if !nonblock { + t.Error("pipe blocking after NewFile") + } + fd := f.Fd() + if fd != uintptr(p[0]) { + t.Errorf("Fd returned %d, want %d", fd, p[0]) + } + nonblock, err = unix.IsNonblock(p[0]) + if err != nil { + t.Fatal(err) + } + if !nonblock { + t.Error("pipe blocking after Fd") + } +} diff --git a/src/os/file.go b/src/os/file.go new file mode 100644 index 0000000..7fd2f5d --- /dev/null +++ b/src/os/file.go @@ -0,0 +1,770 @@ +// Copyright 2009 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 os provides a platform-independent interface to operating system +// functionality. The design is Unix-like, although the error handling is +// Go-like; failing calls return values of type error rather than error numbers. +// Often, more information is available within the error. For example, +// if a call that takes a file name fails, such as Open or Stat, the error +// will include the failing file name when printed and will be of type +// *PathError, which may be unpacked for more information. +// +// The os interface is intended to be uniform across all operating systems. +// Features not generally available appear in the system-specific package syscall. +// +// Here is a simple example, opening a file and reading some of it. +// +// file, err := os.Open("file.go") // For read access. +// if err != nil { +// log.Fatal(err) +// } +// +// If the open fails, the error string will be self-explanatory, like +// +// open file.go: no such file or directory +// +// The file's data can then be read into a slice of bytes. Read and +// Write take their byte counts from the length of the argument slice. +// +// data := make([]byte, 100) +// count, err := file.Read(data) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Printf("read %d bytes: %q\n", count, data[:count]) +// +// Note: The maximum number of concurrent operations on a File may be limited by +// the OS or the system. The number should be high, but exceeding it may degrade +// performance or cause other issues. +package os + +import ( + "errors" + "internal/poll" + "internal/safefilepath" + "internal/testlog" + "io" + "io/fs" + "runtime" + "syscall" + "time" + "unsafe" +) + +// Name returns the name of the file as presented to Open. +func (f *File) Name() string { return f.name } + +// Stdin, Stdout, and Stderr are open Files pointing to the standard input, +// standard output, and standard error file descriptors. +// +// Note that the Go runtime writes to standard error for panics and crashes; +// closing Stderr may cause those messages to go elsewhere, perhaps +// to a file opened later. +var ( + Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin") + Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout") + Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr") +) + +// Flags to OpenFile wrapping those of the underlying system. Not all +// flags may be implemented on a given system. +const ( + // Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified. + O_RDONLY int = syscall.O_RDONLY // open the file read-only. + O_WRONLY int = syscall.O_WRONLY // open the file write-only. + O_RDWR int = syscall.O_RDWR // open the file read-write. + // The remaining values may be or'ed in to control behavior. + O_APPEND int = syscall.O_APPEND // append data to the file when writing. + O_CREATE int = syscall.O_CREAT // create a new file if none exists. + O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist. + O_SYNC int = syscall.O_SYNC // open for synchronous I/O. + O_TRUNC int = syscall.O_TRUNC // truncate regular writable file when opened. +) + +// Seek whence values. +// +// Deprecated: Use io.SeekStart, io.SeekCurrent, and io.SeekEnd. +const ( + SEEK_SET int = 0 // seek relative to the origin of the file + SEEK_CUR int = 1 // seek relative to the current offset + SEEK_END int = 2 // seek relative to the end +) + +// LinkError records an error during a link or symlink or rename +// system call and the paths that caused it. +type LinkError struct { + Op string + Old string + New string + Err error +} + +func (e *LinkError) Error() string { + return e.Op + " " + e.Old + " " + e.New + ": " + e.Err.Error() +} + +func (e *LinkError) Unwrap() error { + return e.Err +} + +// Read reads up to len(b) bytes from the File and stores them in b. +// 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) { + if err := f.checkValid("read"); err != nil { + return 0, err + } + n, e := f.read(b) + return n, f.wrapErr("read", e) +} + +// ReadAt reads len(b) bytes from the File starting at byte offset off. +// It returns the number of bytes read and the error, if any. +// ReadAt always returns a non-nil error when n < len(b). +// At end of file, that error is io.EOF. +func (f *File) ReadAt(b []byte, off int64) (n int, err error) { + if err := f.checkValid("read"); err != nil { + return 0, err + } + + if off < 0 { + return 0, &PathError{Op: "readat", Path: f.name, Err: errors.New("negative offset")} + } + + for len(b) > 0 { + m, e := f.pread(b, off) + if e != nil { + err = f.wrapErr("read", e) + break + } + n += m + b = b[m:] + off += int64(m) + } + return +} + +// ReadFrom implements io.ReaderFrom. +func (f *File) ReadFrom(r io.Reader) (n int64, err error) { + if err := f.checkValid("write"); err != nil { + return 0, err + } + n, handled, e := f.readFrom(r) + if !handled { + return genericReadFrom(f, r) // without wrapping + } + return n, f.wrapErr("write", e) +} + +func genericReadFrom(f *File, r io.Reader) (int64, error) { + return io.Copy(fileWithoutReadFrom{f}, r) +} + +// fileWithoutReadFrom implements all the methods of *File other +// than ReadFrom. This is used to permit ReadFrom to call io.Copy +// without leading to a recursive call to ReadFrom. +type fileWithoutReadFrom struct { + *File +} + +// This ReadFrom method hides the *File ReadFrom method. +func (fileWithoutReadFrom) ReadFrom(fileWithoutReadFrom) { + panic("unreachable") +} + +// Write writes len(b) bytes from b 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) { + if err := f.checkValid("write"); err != nil { + return 0, err + } + n, e := f.write(b) + if n < 0 { + n = 0 + } + if n != len(b) { + err = io.ErrShortWrite + } + + epipecheck(f, e) + + if e != nil { + err = f.wrapErr("write", e) + } + + return n, err +} + +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, off int64) (n int, err error) { + if err := f.checkValid("write"); err != nil { + return 0, err + } + if f.appendMode { + return 0, errWriteAtInAppendMode + } + + if off < 0 { + return 0, &PathError{Op: "writeat", Path: f.name, Err: errors.New("negative offset")} + } + + for len(b) > 0 { + m, e := f.pwrite(b, off) + if e != nil { + err = f.wrapErr("write", e) + break + } + n += m + b = b[m:] + off += int64(m) + } + return +} + +// Seek sets the offset for the next Read or Write on file to offset, interpreted +// according to whence: 0 means relative to the origin of the file, 1 means +// relative to the current offset, and 2 means relative to the end. +// It returns the new offset and an error, if any. +// The behavior of Seek on a file opened with O_APPEND is not specified. +func (f *File) Seek(offset int64, whence int) (ret int64, err error) { + if err := f.checkValid("seek"); err != nil { + return 0, err + } + r, e := f.seek(offset, whence) + if e == nil && f.dirinfo != nil && r != 0 { + e = syscall.EISDIR + } + if e != nil { + return 0, f.wrapErr("seek", e) + } + return r, nil +} + +// WriteString is like Write, but writes the contents of string s rather than +// a slice of bytes. +func (f *File) WriteString(s string) (n int, err error) { + b := unsafe.Slice(unsafe.StringData(s), len(s)) + return f.Write(b) +} + +// Mkdir creates a new directory with the specified name and permission +// bits (before umask). +// If there is an error, it will be of type *PathError. +func Mkdir(name string, perm FileMode) error { + longName := fixLongPath(name) + e := ignoringEINTR(func() error { + return syscall.Mkdir(longName, syscallMode(perm)) + }) + + if e != nil { + return &PathError{Op: "mkdir", Path: name, Err: e} + } + + // mkdir(2) itself won't handle the sticky bit on *BSD and Solaris + if !supportsCreateWithStickyBit && perm&ModeSticky != 0 { + e = setStickyBit(name) + + if e != nil { + Remove(name) + return e + } + } + + return nil +} + +// setStickyBit adds ModeSticky to the permission bits of path, non atomic. +func setStickyBit(name string) error { + fi, err := Stat(name) + if err != nil { + return err + } + return Chmod(name, fi.Mode()|ModeSticky) +} + +// Chdir changes the current working directory to the named directory. +// If there is an error, it will be of type *PathError. +func Chdir(dir string) error { + if e := syscall.Chdir(dir); e != nil { + testlog.Open(dir) // observe likely non-existent directory + return &PathError{Op: "chdir", Path: dir, Err: e} + } + if log := testlog.Logger(); log != nil { + wd, err := Getwd() + if err == nil { + log.Chdir(wd) + } + } + return nil +} + +// Open opens the named file for reading. If successful, methods on +// the returned file can be used for reading; the associated file +// descriptor has mode O_RDONLY. +// If there is an error, it will be of type *PathError. +func Open(name string) (*File, error) { + return OpenFile(name, O_RDONLY, 0) +} + +// Create creates or truncates the named file. If the file already exists, +// it is truncated. If the file does not exist, it is created with mode 0666 +// (before umask). If successful, methods on the returned File can +// be used for I/O; the associated file descriptor has mode O_RDWR. +// If there is an error, it will be of type *PathError. +func Create(name string) (*File, error) { + return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666) +} + +// OpenFile is the generalized open call; most users will use Open +// or Create instead. It opens the named file with specified flag +// (O_RDONLY etc.). If the file does not exist, and the O_CREATE flag +// is passed, it is created with mode perm (before umask). If successful, +// methods on the returned File can be used for I/O. +// If there is an error, it will be of type *PathError. +func OpenFile(name string, flag int, perm FileMode) (*File, error) { + testlog.Open(name) + f, err := openFileNolog(name, flag, perm) + if err != nil { + return nil, err + } + f.appendMode = flag&O_APPEND != 0 + + return f, nil +} + +// lstat is overridden in tests. +var lstat = Lstat + +// Rename renames (moves) oldpath to newpath. +// If newpath already exists and is not a directory, Rename replaces it. +// OS-specific restrictions may apply when oldpath and newpath are in different directories. +// Even within the same directory, on non-Unix platforms Rename is not an atomic operation. +// If there is an error, it will be of type *LinkError. +func Rename(oldpath, newpath string) error { + return rename(oldpath, newpath) +} + +// Many functions in package syscall return a count of -1 instead of 0. +// Using fixCount(call()) instead of call() corrects the count. +func fixCount(n int, err error) (int, error) { + if n < 0 { + n = 0 + } + return n, err +} + +// checkWrapErr is the test hook to enable checking unexpected wrapped errors of poll.ErrFileClosing. +// It is set to true in the export_test.go for tests (including fuzz tests). +var checkWrapErr = false + +// wrapErr wraps an error that occurred during an operation on an open file. +// It passes io.EOF through unchanged, otherwise converts +// poll.ErrFileClosing to ErrClosed and wraps the error in a PathError. +func (f *File) wrapErr(op string, err error) error { + if err == nil || err == io.EOF { + return err + } + if err == poll.ErrFileClosing { + err = ErrClosed + } else if checkWrapErr && errors.Is(err, poll.ErrFileClosing) { + panic("unexpected error wrapping poll.ErrFileClosing: " + err.Error()) + } + return &PathError{Op: op, Path: f.name, Err: err} +} + +// TempDir returns the default directory to use for temporary files. +// +// On Unix systems, it returns $TMPDIR if non-empty, else /tmp. +// On Windows, it uses GetTempPath, returning the first non-empty +// value from %TMP%, %TEMP%, %USERPROFILE%, or the Windows directory. +// On Plan 9, it returns /tmp. +// +// The directory is neither guaranteed to exist nor have accessible +// permissions. +func TempDir() string { + return tempDir() +} + +// UserCacheDir returns the default root directory to use for user-specific +// cached data. Users should create their own application-specific subdirectory +// within this one and use that. +// +// On Unix systems, it returns $XDG_CACHE_HOME as specified by +// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if +// non-empty, else $HOME/.cache. +// On Darwin, it returns $HOME/Library/Caches. +// On Windows, it returns %LocalAppData%. +// On Plan 9, it returns $home/lib/cache. +// +// If the location cannot be determined (for example, $HOME is not defined), +// then it will return an error. +func UserCacheDir() (string, error) { + var dir string + + switch runtime.GOOS { + case "windows": + dir = Getenv("LocalAppData") + if dir == "" { + return "", errors.New("%LocalAppData% is not defined") + } + + case "darwin", "ios": + dir = Getenv("HOME") + if dir == "" { + return "", errors.New("$HOME is not defined") + } + dir += "/Library/Caches" + + case "plan9": + dir = Getenv("home") + if dir == "" { + return "", errors.New("$home is not defined") + } + dir += "/lib/cache" + + default: // Unix + dir = Getenv("XDG_CACHE_HOME") + if dir == "" { + dir = Getenv("HOME") + if dir == "" { + return "", errors.New("neither $XDG_CACHE_HOME nor $HOME are defined") + } + dir += "/.cache" + } + } + + return dir, nil +} + +// UserConfigDir returns the default root directory to use for user-specific +// configuration data. Users should create their own application-specific +// subdirectory within this one and use that. +// +// On Unix systems, it returns $XDG_CONFIG_HOME as specified by +// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if +// non-empty, else $HOME/.config. +// On Darwin, it returns $HOME/Library/Application Support. +// On Windows, it returns %AppData%. +// On Plan 9, it returns $home/lib. +// +// If the location cannot be determined (for example, $HOME is not defined), +// then it will return an error. +func UserConfigDir() (string, error) { + var dir string + + switch runtime.GOOS { + case "windows": + dir = Getenv("AppData") + if dir == "" { + return "", errors.New("%AppData% is not defined") + } + + case "darwin", "ios": + dir = Getenv("HOME") + if dir == "" { + return "", errors.New("$HOME is not defined") + } + dir += "/Library/Application Support" + + case "plan9": + dir = Getenv("home") + if dir == "" { + return "", errors.New("$home is not defined") + } + dir += "/lib" + + default: // Unix + dir = Getenv("XDG_CONFIG_HOME") + if dir == "" { + dir = Getenv("HOME") + if dir == "" { + return "", errors.New("neither $XDG_CONFIG_HOME nor $HOME are defined") + } + dir += "/.config" + } + } + + return dir, nil +} + +// UserHomeDir returns the current user's home directory. +// +// On Unix, including macOS, it returns the $HOME environment variable. +// On Windows, it returns %USERPROFILE%. +// On Plan 9, it returns the $home environment variable. +// +// If the expected variable is not set in the environment, UserHomeDir +// returns either a platform-specific default value or a non-nil error. +func UserHomeDir() (string, error) { + env, enverr := "HOME", "$HOME" + switch runtime.GOOS { + case "windows": + env, enverr = "USERPROFILE", "%userprofile%" + case "plan9": + env, enverr = "home", "$home" + } + if v := Getenv(env); v != "" { + return v, nil + } + // On some geese the home directory is not always defined. + switch runtime.GOOS { + case "android": + return "/sdcard", nil + case "ios": + return "/", nil + } + return "", errors.New(enverr + " is not defined") +} + +// Chmod changes the mode of the named file to mode. +// If the file is a symbolic link, it changes the mode of the link's target. +// If there is an error, it will be of type *PathError. +// +// A different subset of the mode bits are used, depending on the +// operating system. +// +// On Unix, the mode's permission bits, ModeSetuid, ModeSetgid, and +// ModeSticky are used. +// +// On Windows, only the 0200 bit (owner writable) of mode is used; it +// controls whether the file's read-only attribute is set or cleared. +// The other bits are currently unused. For compatibility with Go 1.12 +// and earlier, use a non-zero mode. Use mode 0400 for a read-only +// file and 0600 for a readable+writable file. +// +// On Plan 9, the mode's permission bits, ModeAppend, ModeExclusive, +// and ModeTemporary are used. +func Chmod(name string, mode FileMode) error { return chmod(name, mode) } + +// Chmod changes the mode of the file to mode. +// If there is an error, it will be of type *PathError. +func (f *File) Chmod(mode FileMode) error { return f.chmod(mode) } + +// SetDeadline sets the read and write deadlines for a File. +// It is equivalent to calling both SetReadDeadline and SetWriteDeadline. +// +// Only some kinds of files support setting a deadline. Calls to SetDeadline +// for files that do not support deadlines will return ErrNoDeadline. +// On most systems ordinary files do not support deadlines, but pipes do. +// +// A deadline is an absolute time after which I/O operations fail with an +// error instead of blocking. The deadline applies to all future and pending +// I/O, not just the immediately following call to Read or Write. +// After a deadline has been exceeded, the connection can be refreshed +// by setting a deadline in the future. +// +// If the deadline is exceeded a call to Read or Write or to other I/O +// methods will return an error that wraps ErrDeadlineExceeded. +// This can be tested using errors.Is(err, os.ErrDeadlineExceeded). +// That error implements the Timeout method, and calling the Timeout +// method will return true, but there are other possible errors for which +// the Timeout will return true even if the deadline has not been exceeded. +// +// An idle timeout can be implemented by repeatedly extending +// the deadline after successful Read or Write calls. +// +// A zero value for t means I/O operations will not time out. +func (f *File) SetDeadline(t time.Time) error { + return f.setDeadline(t) +} + +// SetReadDeadline sets the deadline for future Read calls and any +// currently-blocked Read call. +// A zero value for t means Read will not time out. +// Not all files support setting deadlines; see SetDeadline. +func (f *File) SetReadDeadline(t time.Time) error { + return f.setReadDeadline(t) +} + +// SetWriteDeadline sets the deadline for any future Write calls and any +// currently-blocked Write call. +// Even if Write times out, it may return n > 0, indicating that +// some of the data was successfully written. +// A zero value for t means Write will not time out. +// Not all files support setting deadlines; see SetDeadline. +func (f *File) SetWriteDeadline(t time.Time) error { + return f.setWriteDeadline(t) +} + +// SyscallConn returns a raw file. +// This implements the syscall.Conn interface. +func (f *File) SyscallConn() (syscall.RawConn, error) { + if err := f.checkValid("SyscallConn"); err != nil { + return nil, err + } + return newRawConn(f) +} + +// DirFS returns a file system (an fs.FS) for the tree of files rooted at the directory dir. +// +// Note that DirFS("/prefix") only guarantees that the Open calls it makes to the +// operating system will begin with "/prefix": DirFS("/prefix").Open("file") is the +// same as os.Open("/prefix/file"). So if /prefix/file is a symbolic link pointing outside +// the /prefix tree, then using DirFS does not stop the access any more than using +// os.Open does. Additionally, the root of the fs.FS returned for a relative path, +// DirFS("prefix"), will be affected by later calls to Chdir. DirFS is therefore not +// a general substitute for a chroot-style security mechanism when the directory tree +// contains arbitrary content. +// +// The directory dir must not be "". +// +// The result implements [io/fs.StatFS], [io/fs.ReadFileFS] and +// [io/fs.ReadDirFS]. +func DirFS(dir string) fs.FS { + return dirFS(dir) +} + +// containsAny reports whether any bytes in chars are within s. +func containsAny(s, chars string) bool { + for i := 0; i < len(s); i++ { + for j := 0; j < len(chars); j++ { + if s[i] == chars[j] { + return true + } + } + } + return false +} + +type dirFS string + +func (dir dirFS) Open(name string) (fs.File, error) { + fullname, err := dir.join(name) + if err != nil { + return nil, &PathError{Op: "stat", Path: name, Err: err} + } + f, err := Open(fullname) + if err != nil { + // DirFS takes a string appropriate for GOOS, + // while the name argument here is always slash separated. + // dir.join will have mixed the two; undo that for + // error reporting. + err.(*PathError).Path = name + return nil, err + } + return f, nil +} + +// The ReadFile method calls the [ReadFile] function for the file +// with the given name in the directory. The function provides +// robust handling for small files and special file systems. +// Through this method, dirFS implements [io/fs.ReadFileFS]. +func (dir dirFS) ReadFile(name string) ([]byte, error) { + fullname, err := dir.join(name) + if err != nil { + return nil, &PathError{Op: "readfile", Path: name, Err: err} + } + return ReadFile(fullname) +} + +// ReadDir reads the named directory, returning all its directory entries sorted +// by filename. Through this method, dirFS implements [io/fs.ReadDirFS]. +func (dir dirFS) ReadDir(name string) ([]DirEntry, error) { + fullname, err := dir.join(name) + if err != nil { + return nil, &PathError{Op: "readdir", Path: name, Err: err} + } + return ReadDir(fullname) +} + +func (dir dirFS) Stat(name string) (fs.FileInfo, error) { + fullname, err := dir.join(name) + if err != nil { + return nil, &PathError{Op: "stat", Path: name, Err: err} + } + f, err := Stat(fullname) + if err != nil { + // See comment in dirFS.Open. + err.(*PathError).Path = name + return nil, err + } + return f, nil +} + +// join returns the path for name in dir. +func (dir dirFS) join(name string) (string, error) { + if dir == "" { + return "", errors.New("os: DirFS with empty root") + } + if !fs.ValidPath(name) { + return "", ErrInvalid + } + name, err := safefilepath.FromFS(name) + if err != nil { + return "", ErrInvalid + } + if IsPathSeparator(dir[len(dir)-1]) { + return string(dir) + name, nil + } + return string(dir) + string(PathSeparator) + name, nil +} + +// ReadFile reads the named file and returns the contents. +// A successful call returns err == nil, not err == EOF. +// Because ReadFile reads the whole file, it does not treat an EOF from Read +// as an error to be reported. +func ReadFile(name string) ([]byte, error) { + f, err := Open(name) + if err != nil { + return nil, err + } + defer f.Close() + + var size int + if info, err := f.Stat(); err == nil { + size64 := info.Size() + if int64(int(size64)) == size64 { + size = int(size64) + } + } + size++ // one byte for final read at EOF + + // If a file claims a small size, read at least 512 bytes. + // In particular, files in Linux's /proc claim size 0 but + // then do not work right if read in small pieces, + // so an initial read of 1 byte would not work correctly. + if size < 512 { + size = 512 + } + + data := make([]byte, 0, size) + for { + if len(data) >= cap(data) { + d := append(data[:cap(data)], 0) + data = d[:len(data)] + } + n, err := f.Read(data[len(data):cap(data)]) + data = data[:len(data)+n] + if err != nil { + if err == io.EOF { + err = nil + } + return data, err + } + } +} + +// WriteFile writes data to the named file, creating it if necessary. +// If the file does not exist, WriteFile creates it with permissions perm (before umask); +// otherwise WriteFile truncates it before writing, without changing permissions. +// Since WriteFile requires multiple system calls to complete, a failure mid-operation +// can leave the file in a partially written state. +func WriteFile(name string, data []byte, perm FileMode) error { + f, err := OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, perm) + if err != nil { + return err + } + _, err = f.Write(data) + if err1 := f.Close(); err1 != nil && err == nil { + err = err1 + } + return err +} diff --git a/src/os/file_mutex_plan9.go b/src/os/file_mutex_plan9.go new file mode 100644 index 0000000..26bf5a7 --- /dev/null +++ b/src/os/file_mutex_plan9.go @@ -0,0 +1,70 @@ +// Copyright 2022 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 os + +// File locking support for Plan 9. This uses fdMutex from the +// internal/poll package. + +// incref adds a reference to the file. It returns an error if the file +// is already closed. This method is on File so that we can incorporate +// a nil test. +func (f *File) incref(op string) (err error) { + if f == nil { + return ErrInvalid + } + if !f.fdmu.Incref() { + err = ErrClosed + if op != "" { + err = &PathError{Op: op, Path: f.name, Err: err} + } + } + return err +} + +// decref removes a reference to the file. If this is the last +// remaining reference, and the file has been marked to be closed, +// then actually close it. +func (file *file) decref() error { + if file.fdmu.Decref() { + return file.destroy() + } + return nil +} + +// readLock adds a reference to the file and locks it for reading. +// It returns an error if the file is already closed. +func (file *file) readLock() error { + if !file.fdmu.ReadLock() { + return ErrClosed + } + return nil +} + +// readUnlock removes a reference from the file and unlocks it for reading. +// It also closes the file if it marked as closed and there is no remaining +// reference. +func (file *file) readUnlock() { + if file.fdmu.ReadUnlock() { + file.destroy() + } +} + +// writeLock adds a reference to the file and locks it for writing. +// It returns an error if the file is already closed. +func (file *file) writeLock() error { + if !file.fdmu.WriteLock() { + return ErrClosed + } + return nil +} + +// writeUnlock removes a reference from the file and unlocks it for writing. +// It also closes the file if it is marked as closed and there is no remaining +// reference. +func (file *file) writeUnlock() { + if file.fdmu.WriteUnlock() { + file.destroy() + } +} diff --git a/src/os/file_open_unix.go b/src/os/file_open_unix.go new file mode 100644 index 0000000..a3336ea --- /dev/null +++ b/src/os/file_open_unix.go @@ -0,0 +1,17 @@ +// 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. + +//go:build unix || (js && wasm) + +package os + +import ( + "internal/poll" + "syscall" +) + +func open(path string, flag int, perm uint32) (int, poll.SysFile, error) { + fd, err := syscall.Open(path, flag, perm) + return fd, poll.SysFile{}, err +} diff --git a/src/os/file_open_wasip1.go b/src/os/file_open_wasip1.go new file mode 100644 index 0000000..f3ef165 --- /dev/null +++ b/src/os/file_open_wasip1.go @@ -0,0 +1,31 @@ +// 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. + +//go:build wasip1 + +package os + +import ( + "internal/poll" + "syscall" +) + +func open(filePath string, flag int, perm uint32) (int, poll.SysFile, error) { + if filePath == "" { + return -1, poll.SysFile{}, syscall.EINVAL + } + absPath := filePath + // os.(*File).Chdir is emulated by setting the working directory to the + // absolute path that this file was opened at, which is why we have to + // resolve and capture it here. + if filePath[0] != '/' { + wd, err := syscall.Getwd() + if err != nil { + return -1, poll.SysFile{}, err + } + absPath = joinPath(wd, filePath) + } + fd, err := syscall.Open(absPath, flag, perm) + return fd, poll.SysFile{Path: absPath}, err +} diff --git a/src/os/file_plan9.go b/src/os/file_plan9.go new file mode 100644 index 0000000..8336487 --- /dev/null +++ b/src/os/file_plan9.go @@ -0,0 +1,620 @@ +// Copyright 2011 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 os + +import ( + "internal/poll" + "io" + "runtime" + "syscall" + "time" +) + +// fixLongPath is a noop on non-Windows platforms. +func fixLongPath(path string) string { + return path +} + +// file is the real representation of *File. +// The extra level of indirection ensures that no clients of os +// can overwrite this data, which could cause the finalizer +// to close the wrong file descriptor. +type file struct { + fdmu poll.FDMutex + fd int + name string + dirinfo *dirInfo // nil unless directory being read + appendMode bool // whether file is opened for appending +} + +// Fd returns the integer Plan 9 file descriptor referencing the open file. +// If f is closed, the file descriptor becomes invalid. +// If f is garbage collected, a finalizer may close the file descriptor, +// making it invalid; see runtime.SetFinalizer for more information on when +// a finalizer might be run. On Unix systems this will cause the SetDeadline +// methods to stop working. +// +// As an alternative, see the f.SyscallConn method. +func (f *File) Fd() uintptr { + if f == nil { + return ^(uintptr(0)) + } + return uintptr(f.fd) +} + +// NewFile returns a new File with the given file descriptor and +// name. The returned value will be nil if fd is not a valid file +// descriptor. +func NewFile(fd uintptr, name string) *File { + fdi := int(fd) + if fdi < 0 { + return nil + } + f := &File{&file{fd: fdi, name: name}} + runtime.SetFinalizer(f.file, (*file).close) + return f +} + +// Auxiliary information if the File describes a directory +type dirInfo struct { + buf [syscall.STATMAX]byte // buffer for directory I/O + nbuf int // length of buf; return value from Read + bufp int // location of next record in buf. +} + +func epipecheck(file *File, e error) { +} + +// DevNull is the name of the operating system's “null device.” +// On Unix-like systems, it is "/dev/null"; on Windows, "NUL". +const DevNull = "/dev/null" + +// syscallMode returns the syscall-specific mode bits from Go's portable mode bits. +func syscallMode(i FileMode) (o uint32) { + o |= uint32(i.Perm()) + if i&ModeAppend != 0 { + o |= syscall.DMAPPEND + } + if i&ModeExclusive != 0 { + o |= syscall.DMEXCL + } + if i&ModeTemporary != 0 { + o |= syscall.DMTMP + } + return +} + +// openFileNolog is the Plan 9 implementation of OpenFile. +func openFileNolog(name string, flag int, perm FileMode) (*File, error) { + var ( + fd int + e error + create bool + excl bool + trunc bool + append bool + ) + + if flag&O_CREATE == O_CREATE { + flag = flag & ^O_CREATE + create = true + } + if flag&O_EXCL == O_EXCL { + excl = true + } + if flag&O_TRUNC == O_TRUNC { + trunc = true + } + // O_APPEND is emulated on Plan 9 + if flag&O_APPEND == O_APPEND { + flag = flag &^ O_APPEND + append = true + } + + if (create && trunc) || excl { + fd, e = syscall.Create(name, flag, syscallMode(perm)) + } else { + fd, e = syscall.Open(name, flag) + if IsNotExist(e) && create { + fd, e = syscall.Create(name, flag, syscallMode(perm)) + if e != nil { + return nil, &PathError{Op: "create", Path: name, Err: e} + } + } + } + + if e != nil { + return nil, &PathError{Op: "open", Path: name, Err: e} + } + + if append { + if _, e = syscall.Seek(fd, 0, io.SeekEnd); e != nil { + return nil, &PathError{Op: "seek", Path: name, Err: e} + } + } + + return NewFile(uintptr(fd), name), nil +} + +// Close closes the File, rendering it unusable for I/O. +// On files that support SetDeadline, any pending I/O operations will +// be canceled and return immediately with an ErrClosed error. +// Close will return an error if it has already been called. +func (f *File) Close() error { + if f == nil { + return ErrInvalid + } + return f.file.close() +} + +func (file *file) close() error { + if !file.fdmu.IncrefAndClose() { + return &PathError{Op: "close", Path: file.name, Err: ErrClosed} + } + + // At this point we should cancel any pending I/O. + // How do we do that on Plan 9? + + err := file.decref() + + // no need for a finalizer anymore + runtime.SetFinalizer(file, nil) + return err +} + +// destroy actually closes the descriptor. This is called when +// there are no remaining references, by the decref, readUnlock, +// and writeUnlock methods. +func (file *file) destroy() error { + var err error + if e := syscall.Close(file.fd); e != nil { + err = &PathError{Op: "close", Path: file.name, Err: e} + } + return err +} + +// Stat returns the FileInfo structure describing file. +// If there is an error, it will be of type *PathError. +func (f *File) Stat() (FileInfo, error) { + if f == nil { + return nil, ErrInvalid + } + d, err := dirstat(f) + if err != nil { + return nil, err + } + return fileInfoFromStat(d), nil +} + +// Truncate changes the size of the file. +// It does not change the I/O offset. +// If there is an error, it will be of type *PathError. +func (f *File) Truncate(size int64) error { + if f == nil { + return ErrInvalid + } + + var d syscall.Dir + d.Null() + d.Length = size + + var buf [syscall.STATFIXLEN]byte + n, err := d.Marshal(buf[:]) + if err != nil { + return &PathError{Op: "truncate", Path: f.name, Err: err} + } + + if err := f.incref("truncate"); err != nil { + return err + } + defer f.decref() + + if err = syscall.Fwstat(f.fd, buf[:n]); err != nil { + return &PathError{Op: "truncate", Path: f.name, Err: err} + } + return nil +} + +const chmodMask = uint32(syscall.DMAPPEND | syscall.DMEXCL | syscall.DMTMP | ModePerm) + +func (f *File) chmod(mode FileMode) error { + if f == nil { + return ErrInvalid + } + var d syscall.Dir + + odir, e := dirstat(f) + if e != nil { + return &PathError{Op: "chmod", Path: f.name, Err: e} + } + d.Null() + d.Mode = odir.Mode&^chmodMask | syscallMode(mode)&chmodMask + + var buf [syscall.STATFIXLEN]byte + n, err := d.Marshal(buf[:]) + if err != nil { + return &PathError{Op: "chmod", Path: f.name, Err: err} + } + + if err := f.incref("chmod"); err != nil { + return err + } + defer f.decref() + + if err = syscall.Fwstat(f.fd, buf[:n]); err != nil { + return &PathError{Op: "chmod", Path: f.name, Err: err} + } + return nil +} + +// Sync commits the current contents of the file to stable storage. +// Typically, this means flushing the file system's in-memory copy +// of recently written data to disk. +func (f *File) Sync() error { + if f == nil { + return ErrInvalid + } + var d syscall.Dir + d.Null() + + var buf [syscall.STATFIXLEN]byte + n, err := d.Marshal(buf[:]) + if err != nil { + return &PathError{Op: "sync", Path: f.name, Err: err} + } + + if err := f.incref("sync"); err != nil { + return err + } + defer f.decref() + + if err = syscall.Fwstat(f.fd, buf[:n]); err != nil { + return &PathError{Op: "sync", Path: f.name, Err: err} + } + return nil +} + +// read reads up to len(b) bytes from the File. +// It returns the number of bytes read and an error, if any. +func (f *File) read(b []byte) (n int, err error) { + if err := f.readLock(); err != nil { + return 0, err + } + defer f.readUnlock() + n, e := fixCount(syscall.Read(f.fd, b)) + if n == 0 && len(b) > 0 && e == nil { + return 0, io.EOF + } + return n, e +} + +// pread reads len(b) bytes from the File starting at byte offset off. +// It returns the number of bytes read and the error, if any. +// EOF is signaled by a zero count with err set to nil. +func (f *File) pread(b []byte, off int64) (n int, err error) { + if err := f.readLock(); err != nil { + return 0, err + } + defer f.readUnlock() + n, e := fixCount(syscall.Pread(f.fd, b, off)) + if n == 0 && len(b) > 0 && e == nil { + return 0, io.EOF + } + return n, e +} + +// write writes len(b) bytes to the File. +// It returns the number of bytes written and an error, if any. +// Since Plan 9 preserves message boundaries, never allow +// a zero-byte write. +func (f *File) write(b []byte) (n int, err error) { + if err := f.writeLock(); err != nil { + return 0, err + } + defer f.writeUnlock() + if len(b) == 0 { + return 0, nil + } + return fixCount(syscall.Write(f.fd, b)) +} + +// pwrite writes len(b) bytes to the File starting at byte offset off. +// It returns the number of bytes written and an error, if any. +// Since Plan 9 preserves message boundaries, never allow +// a zero-byte write. +func (f *File) pwrite(b []byte, off int64) (n int, err error) { + if err := f.writeLock(); err != nil { + return 0, err + } + defer f.writeUnlock() + if len(b) == 0 { + return 0, nil + } + return fixCount(syscall.Pwrite(f.fd, b, off)) +} + +// seek sets the offset for the next Read or Write on file to offset, interpreted +// according to whence: 0 means relative to the origin of the file, 1 means +// relative to the current offset, and 2 means relative to the end. +// It returns the new offset and an error, if any. +func (f *File) seek(offset int64, whence int) (ret int64, err error) { + if err := f.incref(""); err != nil { + return 0, err + } + defer f.decref() + if f.dirinfo != nil { + // Free cached dirinfo, so we allocate a new one if we + // access this file as a directory again. See #35767 and #37161. + f.dirinfo = nil + } + return syscall.Seek(f.fd, offset, whence) +} + +// Truncate changes the size of the named file. +// If the file is a symbolic link, it changes the size of the link's target. +// If there is an error, it will be of type *PathError. +func Truncate(name string, size int64) error { + var d syscall.Dir + + d.Null() + d.Length = size + + var buf [syscall.STATFIXLEN]byte + n, err := d.Marshal(buf[:]) + if err != nil { + return &PathError{Op: "truncate", Path: name, Err: err} + } + if err = syscall.Wstat(name, buf[:n]); err != nil { + return &PathError{Op: "truncate", Path: name, Err: err} + } + return nil +} + +// Remove removes the named file or directory. +// If there is an error, it will be of type *PathError. +func Remove(name string) error { + if e := syscall.Remove(name); e != nil { + return &PathError{Op: "remove", Path: name, Err: e} + } + return nil +} + +// hasPrefix from the strings package. +func hasPrefix(s, prefix string) bool { + return len(s) >= len(prefix) && s[0:len(prefix)] == prefix +} + +func rename(oldname, newname string) error { + dirname := oldname[:lastIndex(oldname, '/')+1] + if hasPrefix(newname, dirname) { + newname = newname[len(dirname):] + } else { + return &LinkError{"rename", oldname, newname, ErrInvalid} + } + + // If newname still contains slashes after removing the oldname + // prefix, the rename is cross-directory and must be rejected. + if lastIndex(newname, '/') >= 0 { + return &LinkError{"rename", oldname, newname, ErrInvalid} + } + + var d syscall.Dir + + d.Null() + d.Name = newname + + buf := make([]byte, syscall.STATFIXLEN+len(d.Name)) + n, err := d.Marshal(buf[:]) + if err != nil { + return &LinkError{"rename", oldname, newname, err} + } + + // If newname already exists and is not a directory, rename replaces it. + f, err := Stat(dirname + newname) + if err == nil && !f.IsDir() { + Remove(dirname + newname) + } + + if err = syscall.Wstat(oldname, buf[:n]); err != nil { + return &LinkError{"rename", oldname, newname, err} + } + return nil +} + +// See docs in file.go:Chmod. +func chmod(name string, mode FileMode) error { + var d syscall.Dir + + odir, e := dirstat(name) + if e != nil { + return &PathError{Op: "chmod", Path: name, Err: e} + } + d.Null() + d.Mode = odir.Mode&^chmodMask | syscallMode(mode)&chmodMask + + var buf [syscall.STATFIXLEN]byte + n, err := d.Marshal(buf[:]) + if err != nil { + return &PathError{Op: "chmod", Path: name, Err: err} + } + if err = syscall.Wstat(name, buf[:n]); err != nil { + return &PathError{Op: "chmod", Path: name, Err: err} + } + return nil +} + +// Chtimes changes the access and modification times of the named +// file, similar to the Unix utime() or utimes() functions. +// A zero time.Time value will leave the corresponding file time unchanged. +// +// The underlying filesystem may truncate or round the values to a +// less precise time unit. +// If there is an error, it will be of type *PathError. +func Chtimes(name string, atime time.Time, mtime time.Time) error { + var d syscall.Dir + + d.Null() + d.Atime = uint32(atime.Unix()) + d.Mtime = uint32(mtime.Unix()) + if atime.IsZero() { + d.Atime = 0xFFFFFFFF + } + if mtime.IsZero() { + d.Mtime = 0xFFFFFFFF + } + + var buf [syscall.STATFIXLEN]byte + n, err := d.Marshal(buf[:]) + if err != nil { + return &PathError{Op: "chtimes", Path: name, Err: err} + } + if err = syscall.Wstat(name, buf[:n]); err != nil { + return &PathError{Op: "chtimes", Path: name, Err: err} + } + return nil +} + +// Pipe returns a connected pair of Files; reads from r return bytes +// written to w. It returns the files and an error, if any. +func Pipe() (r *File, w *File, err error) { + var p [2]int + + if e := syscall.Pipe(p[0:]); e != nil { + return nil, nil, NewSyscallError("pipe", e) + } + + return NewFile(uintptr(p[0]), "|0"), NewFile(uintptr(p[1]), "|1"), nil +} + +// not supported on Plan 9 + +// Link creates newname as a hard link to the oldname file. +// If there is an error, it will be of type *LinkError. +func Link(oldname, newname string) error { + return &LinkError{"link", oldname, newname, syscall.EPLAN9} +} + +// Symlink creates newname as a symbolic link to oldname. +// On Windows, a symlink to a non-existent oldname creates a file symlink; +// if oldname is later created as a directory the symlink will not work. +// If there is an error, it will be of type *LinkError. +func Symlink(oldname, newname string) error { + return &LinkError{"symlink", oldname, newname, syscall.EPLAN9} +} + +// Readlink returns the destination of the named symbolic link. +// If there is an error, it will be of type *PathError. +func Readlink(name string) (string, error) { + return "", &PathError{Op: "readlink", Path: name, Err: syscall.EPLAN9} +} + +// Chown changes the numeric uid and gid of the named file. +// If the file is a symbolic link, it changes the uid and gid of the link's target. +// A uid or gid of -1 means to not change that value. +// If there is an error, it will be of type *PathError. +// +// On Windows or Plan 9, Chown always returns the syscall.EWINDOWS or +// EPLAN9 error, wrapped in *PathError. +func Chown(name string, uid, gid int) error { + return &PathError{Op: "chown", Path: name, Err: syscall.EPLAN9} +} + +// Lchown changes the numeric uid and gid of the named file. +// If the file is a symbolic link, it changes the uid and gid of the link itself. +// If there is an error, it will be of type *PathError. +func Lchown(name string, uid, gid int) error { + return &PathError{Op: "lchown", Path: name, Err: syscall.EPLAN9} +} + +// Chown changes the numeric uid and gid of the named file. +// If there is an error, it will be of type *PathError. +func (f *File) Chown(uid, gid int) error { + if f == nil { + return ErrInvalid + } + return &PathError{Op: "chown", Path: f.name, Err: syscall.EPLAN9} +} + +func tempDir() string { + dir := Getenv("TMPDIR") + if dir == "" { + dir = "/tmp" + } + return dir + +} + +// Chdir changes the current working directory to the file, +// which must be a directory. +// If there is an error, it will be of type *PathError. +func (f *File) Chdir() error { + if err := f.incref("chdir"); err != nil { + return err + } + defer f.decref() + if e := syscall.Fchdir(f.fd); e != nil { + return &PathError{Op: "chdir", Path: f.name, Err: e} + } + return nil +} + +// setDeadline sets the read and write deadline. +func (f *File) setDeadline(time.Time) error { + if err := f.checkValid("SetDeadline"); err != nil { + return err + } + return poll.ErrNoDeadline +} + +// setReadDeadline sets the read deadline. +func (f *File) setReadDeadline(time.Time) error { + if err := f.checkValid("SetReadDeadline"); err != nil { + return err + } + return poll.ErrNoDeadline +} + +// setWriteDeadline sets the write deadline. +func (f *File) setWriteDeadline(time.Time) error { + if err := f.checkValid("SetWriteDeadline"); err != nil { + return err + } + return poll.ErrNoDeadline +} + +// checkValid checks whether f is valid for use, but does not prepare +// to actually use it. If f is not ready checkValid returns an appropriate +// error, perhaps incorporating the operation name op. +func (f *File) checkValid(op string) error { + if f == nil { + return ErrInvalid + } + if err := f.incref(op); err != nil { + return err + } + return f.decref() +} + +type rawConn struct{} + +func (c *rawConn) Control(f func(uintptr)) error { + return syscall.EPLAN9 +} + +func (c *rawConn) Read(f func(uintptr) bool) error { + return syscall.EPLAN9 +} + +func (c *rawConn) Write(f func(uintptr) bool) error { + return syscall.EPLAN9 +} + +func newRawConn(file *File) (*rawConn, error) { + return nil, syscall.EPLAN9 +} + +func ignoringEINTR(fn func() error) error { + return fn() +} diff --git a/src/os/file_posix.go b/src/os/file_posix.go new file mode 100644 index 0000000..5692657 --- /dev/null +++ b/src/os/file_posix.go @@ -0,0 +1,256 @@ +// Copyright 2009 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. + +//go:build unix || (js && wasm) || wasip1 || windows + +package os + +import ( + "runtime" + "syscall" + "time" +) + +// Close closes the File, rendering it unusable for I/O. +// On files that support SetDeadline, any pending I/O operations will +// be canceled and return immediately with an ErrClosed error. +// Close will return an error if it has already been called. +func (f *File) Close() error { + if f == nil { + return ErrInvalid + } + return f.file.close() +} + +// read reads up to len(b) bytes from the File. +// It returns the number of bytes read and an error, if any. +func (f *File) read(b []byte) (n int, err error) { + n, err = f.pfd.Read(b) + runtime.KeepAlive(f) + return n, err +} + +// pread reads len(b) bytes from the File starting at byte offset off. +// It returns the number of bytes read and the error, if any. +// EOF is signaled by a zero count with err set to nil. +func (f *File) pread(b []byte, off int64) (n int, err error) { + n, err = f.pfd.Pread(b, off) + runtime.KeepAlive(f) + return n, err +} + +// write writes len(b) bytes to the File. +// It returns the number of bytes written and an error, if any. +func (f *File) write(b []byte) (n int, err error) { + n, err = f.pfd.Write(b) + runtime.KeepAlive(f) + return n, err +} + +// pwrite writes len(b) bytes to the File starting at byte offset off. +// It returns the number of bytes written and an error, if any. +func (f *File) pwrite(b []byte, off int64) (n int, err error) { + n, err = f.pfd.Pwrite(b, off) + runtime.KeepAlive(f) + return n, err +} + +// syscallMode returns the syscall-specific mode bits from Go's portable mode bits. +func syscallMode(i FileMode) (o uint32) { + o |= uint32(i.Perm()) + if i&ModeSetuid != 0 { + o |= syscall.S_ISUID + } + if i&ModeSetgid != 0 { + o |= syscall.S_ISGID + } + if i&ModeSticky != 0 { + o |= syscall.S_ISVTX + } + // No mapping for Go's ModeTemporary (plan9 only). + return +} + +// See docs in file.go:Chmod. +func chmod(name string, mode FileMode) error { + longName := fixLongPath(name) + e := ignoringEINTR(func() error { + return syscall.Chmod(longName, syscallMode(mode)) + }) + if e != nil { + return &PathError{Op: "chmod", Path: name, Err: e} + } + return nil +} + +// See docs in file.go:(*File).Chmod. +func (f *File) chmod(mode FileMode) error { + if err := f.checkValid("chmod"); err != nil { + return err + } + if e := f.pfd.Fchmod(syscallMode(mode)); e != nil { + return f.wrapErr("chmod", e) + } + return nil +} + +// Chown changes the numeric uid and gid of the named file. +// If the file is a symbolic link, it changes the uid and gid of the link's target. +// A uid or gid of -1 means to not change that value. +// If there is an error, it will be of type *PathError. +// +// On Windows or Plan 9, Chown always returns the syscall.EWINDOWS or +// EPLAN9 error, wrapped in *PathError. +func Chown(name string, uid, gid int) error { + e := ignoringEINTR(func() error { + return syscall.Chown(name, uid, gid) + }) + if e != nil { + return &PathError{Op: "chown", Path: name, Err: e} + } + return nil +} + +// Lchown changes the numeric uid and gid of the named file. +// If the file is a symbolic link, it changes the uid and gid of the link itself. +// If there is an error, it will be of type *PathError. +// +// On Windows, it always returns the syscall.EWINDOWS error, wrapped +// in *PathError. +func Lchown(name string, uid, gid int) error { + e := ignoringEINTR(func() error { + return syscall.Lchown(name, uid, gid) + }) + if e != nil { + return &PathError{Op: "lchown", Path: name, Err: e} + } + return nil +} + +// Chown changes the numeric uid and gid of the named file. +// If there is an error, it will be of type *PathError. +// +// On Windows, it always returns the syscall.EWINDOWS error, wrapped +// in *PathError. +func (f *File) Chown(uid, gid int) error { + if err := f.checkValid("chown"); err != nil { + return err + } + if e := f.pfd.Fchown(uid, gid); e != nil { + return f.wrapErr("chown", e) + } + return nil +} + +// Truncate changes the size of the file. +// It does not change the I/O offset. +// If there is an error, it will be of type *PathError. +func (f *File) Truncate(size int64) error { + if err := f.checkValid("truncate"); err != nil { + return err + } + if e := f.pfd.Ftruncate(size); e != nil { + return f.wrapErr("truncate", e) + } + return nil +} + +// Sync commits the current contents of the file to stable storage. +// Typically, this means flushing the file system's in-memory copy +// of recently written data to disk. +func (f *File) Sync() error { + if err := f.checkValid("sync"); err != nil { + return err + } + if e := f.pfd.Fsync(); e != nil { + return f.wrapErr("sync", e) + } + return nil +} + +// Chtimes changes the access and modification times of the named +// file, similar to the Unix utime() or utimes() functions. +// A zero time.Time value will leave the corresponding file time unchanged. +// +// The underlying filesystem may truncate or round the values to a +// less precise time unit. +// If there is an error, it will be of type *PathError. +func Chtimes(name string, atime time.Time, mtime time.Time) error { + var utimes [2]syscall.Timespec + set := func(i int, t time.Time) { + if t.IsZero() { + utimes[i] = syscall.Timespec{Sec: _UTIME_OMIT, Nsec: _UTIME_OMIT} + } else { + utimes[i] = syscall.NsecToTimespec(t.UnixNano()) + } + } + set(0, atime) + set(1, mtime) + if e := syscall.UtimesNano(fixLongPath(name), utimes[0:]); e != nil { + return &PathError{Op: "chtimes", Path: name, Err: e} + } + return nil +} + +// Chdir changes the current working directory to the file, +// which must be a directory. +// If there is an error, it will be of type *PathError. +func (f *File) Chdir() error { + if err := f.checkValid("chdir"); err != nil { + return err + } + if e := f.pfd.Fchdir(); e != nil { + return f.wrapErr("chdir", e) + } + return nil +} + +// setDeadline sets the read and write deadline. +func (f *File) setDeadline(t time.Time) error { + if err := f.checkValid("SetDeadline"); err != nil { + return err + } + return f.pfd.SetDeadline(t) +} + +// setReadDeadline sets the read deadline. +func (f *File) setReadDeadline(t time.Time) error { + if err := f.checkValid("SetReadDeadline"); err != nil { + return err + } + return f.pfd.SetReadDeadline(t) +} + +// setWriteDeadline sets the write deadline. +func (f *File) setWriteDeadline(t time.Time) error { + if err := f.checkValid("SetWriteDeadline"); err != nil { + return err + } + return f.pfd.SetWriteDeadline(t) +} + +// checkValid checks whether f is valid for use. +// If not, it returns an appropriate error, perhaps incorporating the operation name op. +func (f *File) checkValid(op string) error { + if f == nil { + return ErrInvalid + } + return nil +} + +// ignoringEINTR makes a function call and repeats it if it returns an +// EINTR error. This appears to be required even though we install all +// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846. +// Also #20400 and #36644 are issues in which a signal handler is +// installed without setting SA_RESTART. None of these are the common case, +// but there are enough of them that it seems that we can't avoid +// an EINTR loop. +func ignoringEINTR(fn func() error) error { + for { + err := fn() + if err != syscall.EINTR { + return err + } + } +} diff --git a/src/os/file_unix.go b/src/os/file_unix.go new file mode 100644 index 0000000..533a484 --- /dev/null +++ b/src/os/file_unix.go @@ -0,0 +1,497 @@ +// Copyright 2009 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. + +//go:build unix || (js && wasm) || wasip1 + +package os + +import ( + "internal/poll" + "internal/syscall/unix" + "io/fs" + "runtime" + "syscall" + _ "unsafe" // for go:linkname +) + +const _UTIME_OMIT = unix.UTIME_OMIT + +// fixLongPath is a noop on non-Windows platforms. +func fixLongPath(path string) string { + return path +} + +func rename(oldname, newname string) error { + fi, err := Lstat(newname) + if err == nil && fi.IsDir() { + // There are two independent errors this function can return: + // one for a bad oldname, and one for a bad newname. + // At this point we've determined the newname is bad. + // But just in case oldname is also bad, prioritize returning + // the oldname error because that's what we did historically. + // However, if the old name and new name are not the same, yet + // they refer to the same file, it implies a case-only + // rename on a case-insensitive filesystem, which is ok. + if ofi, err := Lstat(oldname); err != nil { + if pe, ok := err.(*PathError); ok { + err = pe.Err + } + return &LinkError{"rename", oldname, newname, err} + } else if newname == oldname || !SameFile(fi, ofi) { + return &LinkError{"rename", oldname, newname, syscall.EEXIST} + } + } + err = ignoringEINTR(func() error { + return syscall.Rename(oldname, newname) + }) + if err != nil { + return &LinkError{"rename", oldname, newname, err} + } + return nil +} + +// file is the real representation of *File. +// The extra level of indirection ensures that no clients of os +// can overwrite this data, which could cause the finalizer +// to close the wrong file descriptor. +type file struct { + pfd poll.FD + name string + dirinfo *dirInfo // nil unless directory being read + nonblock bool // whether we set nonblocking mode + stdoutOrErr bool // whether this is stdout or stderr + appendMode bool // whether file is opened for appending +} + +// Fd returns the integer Unix file descriptor referencing the open file. +// If f is closed, the file descriptor becomes invalid. +// If f is garbage collected, a finalizer may close the file descriptor, +// making it invalid; see runtime.SetFinalizer for more information on when +// a finalizer might be run. On Unix systems this will cause the SetDeadline +// methods to stop working. +// Because file descriptors can be reused, the returned file descriptor may +// only be closed through the Close method of f, or by its finalizer during +// garbage collection. Otherwise, during garbage collection the finalizer +// may close an unrelated file descriptor with the same (reused) number. +// +// As an alternative, see the f.SyscallConn method. +func (f *File) Fd() uintptr { + if f == nil { + return ^(uintptr(0)) + } + + // If we put the file descriptor into nonblocking mode, + // then set it to blocking mode before we return it, + // because historically we have always returned a descriptor + // opened in blocking mode. The File will continue to work, + // but any blocking operation will tie up a thread. + if f.nonblock { + f.pfd.SetBlocking() + } + + return uintptr(f.pfd.Sysfd) +} + +// NewFile returns a new File with the given file descriptor and +// name. The returned value will be nil if fd is not a valid file +// descriptor. On Unix systems, if the file descriptor is in +// non-blocking mode, NewFile will attempt to return a pollable File +// (one for which the SetDeadline methods work). +// +// After passing it to NewFile, fd may become invalid under the same +// conditions described in the comments of the Fd method, and the same +// constraints apply. +func NewFile(fd uintptr, name string) *File { + fdi := int(fd) + if fdi < 0 { + return nil + } + + kind := kindNewFile + appendMode := false + if flags, err := unix.Fcntl(fdi, syscall.F_GETFL, 0); err == nil { + if unix.HasNonblockFlag(flags) { + kind = kindNonBlock + } + appendMode = flags&syscall.O_APPEND != 0 + } + f := newFile(fdi, name, kind) + f.appendMode = appendMode + return f +} + +// net_newUnixFile is a hidden entry point called by net.conn.File. +// This is used so that a nonblocking network connection will become +// blocking if code calls the Fd method. We don't want that for direct +// calls to NewFile: passing a nonblocking descriptor to NewFile should +// remain nonblocking if you get it back using Fd. But for net.conn.File +// the call to NewFile is hidden from the user. Historically in that case +// the Fd method has returned a blocking descriptor, and we want to +// retain that behavior because existing code expects it and depends on it. +// +//go:linkname net_newUnixFile net.newUnixFile +func net_newUnixFile(fd int, name string) *File { + if fd < 0 { + panic("invalid FD") + } + + f := newFile(fd, name, kindNonBlock) + f.nonblock = true // tell Fd to return blocking descriptor + return f +} + +// newFileKind describes the kind of file to newFile. +type newFileKind int + +const ( + // kindNewFile means that the descriptor was passed to us via NewFile. + kindNewFile newFileKind = iota + // kindOpenFile means that the descriptor was opened using + // Open, Create, or OpenFile (without O_NONBLOCK). + kindOpenFile + // kindPipe means that the descriptor was opened using Pipe. + kindPipe + // kindNonBlock means that the descriptor is already in + // non-blocking mode. + kindNonBlock + // kindNoPoll means that we should not put the descriptor into + // non-blocking mode, because we know it is not a pipe or FIFO. + // Used by openFdAt for directories. + kindNoPoll +) + +// newFile is like NewFile, but if called from OpenFile or Pipe +// (as passed in the kind parameter) it tries to add the file to +// the runtime poller. +func newFile(fd int, name string, kind newFileKind) *File { + f := &File{&file{ + pfd: poll.FD{ + Sysfd: fd, + IsStream: true, + ZeroReadIsEOF: true, + }, + name: name, + stdoutOrErr: fd == 1 || fd == 2, + }} + + pollable := kind == kindOpenFile || kind == kindPipe || kind == kindNonBlock + + // If the caller passed a non-blocking filedes (kindNonBlock), + // we assume they know what they are doing so we allow it to be + // used with kqueue. + if kind == kindOpenFile { + switch runtime.GOOS { + case "darwin", "ios", "dragonfly", "freebsd", "netbsd", "openbsd": + var st syscall.Stat_t + err := ignoringEINTR(func() error { + return syscall.Fstat(fd, &st) + }) + typ := st.Mode & syscall.S_IFMT + // Don't try to use kqueue with regular files on *BSDs. + // On FreeBSD a regular file is always + // reported as ready for writing. + // On Dragonfly, NetBSD and OpenBSD the fd is signaled + // only once as ready (both read and write). + // Issue 19093. + // Also don't add directories to the netpoller. + if err == nil && (typ == syscall.S_IFREG || typ == syscall.S_IFDIR) { + pollable = false + } + + // In addition to the behavior described above for regular files, + // on Darwin, kqueue does not work properly with fifos: + // closing the last writer does not cause a kqueue event + // for any readers. See issue #24164. + if (runtime.GOOS == "darwin" || runtime.GOOS == "ios") && typ == syscall.S_IFIFO { + pollable = false + } + } + } + + clearNonBlock := false + if pollable { + if kind == kindNonBlock { + // The descriptor is already in non-blocking mode. + // We only set f.nonblock if we put the file into + // non-blocking mode. + } else if err := syscall.SetNonblock(fd, true); err == nil { + f.nonblock = true + clearNonBlock = true + } else { + pollable = false + } + } + + // An error here indicates a failure to register + // with the netpoll system. That can happen for + // a file descriptor that is not supported by + // epoll/kqueue; for example, disk files on + // Linux systems. We assume that any real error + // will show up in later I/O. + // We do restore the blocking behavior if it was set by us. + if pollErr := f.pfd.Init("file", pollable); pollErr != nil && clearNonBlock { + if err := syscall.SetNonblock(fd, false); err == nil { + f.nonblock = false + } + } + + runtime.SetFinalizer(f.file, (*file).close) + return f +} + +func sigpipe() // implemented in package runtime + +// epipecheck raises SIGPIPE if we get an EPIPE error on standard +// output or standard error. See the SIGPIPE docs in os/signal, and +// issue 11845. +func epipecheck(file *File, e error) { + if e == syscall.EPIPE && file.stdoutOrErr { + sigpipe() + } +} + +// DevNull is the name of the operating system's “null device.” +// On Unix-like systems, it is "/dev/null"; on Windows, "NUL". +const DevNull = "/dev/null" + +// openFileNolog is the Unix implementation of OpenFile. +// Changes here should be reflected in openFdAt, if relevant. +func openFileNolog(name string, flag int, perm FileMode) (*File, error) { + setSticky := false + if !supportsCreateWithStickyBit && flag&O_CREATE != 0 && perm&ModeSticky != 0 { + if _, err := Stat(name); IsNotExist(err) { + setSticky = true + } + } + + var r int + var s poll.SysFile + for { + var e error + r, s, e = open(name, flag|syscall.O_CLOEXEC, syscallMode(perm)) + if e == nil { + break + } + + // We have to check EINTR here, per issues 11180 and 39237. + if e == syscall.EINTR { + continue + } + + return nil, &PathError{Op: "open", Path: name, Err: e} + } + + // open(2) itself won't handle the sticky bit on *BSD and Solaris + if setSticky { + setStickyBit(name) + } + + // There's a race here with fork/exec, which we are + // content to live with. See ../syscall/exec_unix.go. + if !supportsCloseOnExec { + syscall.CloseOnExec(r) + } + + kind := kindOpenFile + if unix.HasNonblockFlag(flag) { + kind = kindNonBlock + } + + f := newFile(r, name, kind) + f.pfd.SysFile = s + return f, nil +} + +func (file *file) close() error { + if file == nil { + return syscall.EINVAL + } + if file.dirinfo != nil { + file.dirinfo.close() + file.dirinfo = nil + } + var err error + if e := file.pfd.Close(); e != nil { + if e == poll.ErrFileClosing { + e = ErrClosed + } + err = &PathError{Op: "close", Path: file.name, Err: e} + } + + // no need for a finalizer anymore + runtime.SetFinalizer(file, nil) + return err +} + +// seek sets the offset for the next Read or Write on file to offset, interpreted +// according to whence: 0 means relative to the origin of the file, 1 means +// relative to the current offset, and 2 means relative to the end. +// It returns the new offset and an error, if any. +func (f *File) seek(offset int64, whence int) (ret int64, err error) { + if f.dirinfo != nil { + // Free cached dirinfo, so we allocate a new one if we + // access this file as a directory again. See #35767 and #37161. + f.dirinfo.close() + f.dirinfo = nil + } + ret, err = f.pfd.Seek(offset, whence) + runtime.KeepAlive(f) + return ret, err +} + +// Truncate changes the size of the named file. +// If the file is a symbolic link, it changes the size of the link's target. +// If there is an error, it will be of type *PathError. +func Truncate(name string, size int64) error { + e := ignoringEINTR(func() error { + return syscall.Truncate(name, size) + }) + if e != nil { + return &PathError{Op: "truncate", Path: name, Err: e} + } + return nil +} + +// Remove removes the named file or (empty) directory. +// If there is an error, it will be of type *PathError. +func Remove(name string) error { + // System call interface forces us to know + // whether name is a file or directory. + // Try both: it is cheaper on average than + // doing a Stat plus the right one. + e := ignoringEINTR(func() error { + return syscall.Unlink(name) + }) + if e == nil { + return nil + } + e1 := ignoringEINTR(func() error { + return syscall.Rmdir(name) + }) + if e1 == nil { + return nil + } + + // Both failed: figure out which error to return. + // OS X and Linux differ on whether unlink(dir) + // returns EISDIR, so can't use that. However, + // both agree that rmdir(file) returns ENOTDIR, + // so we can use that to decide which error is real. + // Rmdir might also return ENOTDIR if given a bad + // file path, like /etc/passwd/foo, but in that case, + // both errors will be ENOTDIR, so it's okay to + // use the error from unlink. + if e1 != syscall.ENOTDIR { + e = e1 + } + return &PathError{Op: "remove", Path: name, Err: e} +} + +func tempDir() string { + dir := Getenv("TMPDIR") + if dir == "" { + if runtime.GOOS == "android" { + dir = "/data/local/tmp" + } else { + dir = "/tmp" + } + } + return dir +} + +// Link creates newname as a hard link to the oldname file. +// If there is an error, it will be of type *LinkError. +func Link(oldname, newname string) error { + e := ignoringEINTR(func() error { + return syscall.Link(oldname, newname) + }) + if e != nil { + return &LinkError{"link", oldname, newname, e} + } + return nil +} + +// Symlink creates newname as a symbolic link to oldname. +// On Windows, a symlink to a non-existent oldname creates a file symlink; +// if oldname is later created as a directory the symlink will not work. +// If there is an error, it will be of type *LinkError. +func Symlink(oldname, newname string) error { + e := ignoringEINTR(func() error { + return syscall.Symlink(oldname, newname) + }) + if e != nil { + return &LinkError{"symlink", oldname, newname, e} + } + return nil +} + +// Readlink returns the destination of the named symbolic link. +// If there is an error, it will be of type *PathError. +func Readlink(name string) (string, error) { + for len := 128; ; len *= 2 { + b := make([]byte, len) + var ( + n int + e error + ) + for { + n, e = fixCount(syscall.Readlink(name, b)) + if e != syscall.EINTR { + break + } + } + // buffer too small + if (runtime.GOOS == "aix" || runtime.GOOS == "wasip1") && e == syscall.ERANGE { + continue + } + if e != nil { + return "", &PathError{Op: "readlink", Path: name, Err: e} + } + if n < len { + return string(b[0:n]), nil + } + } +} + +type unixDirent struct { + parent string + name string + typ FileMode + info FileInfo +} + +func (d *unixDirent) Name() string { return d.name } +func (d *unixDirent) IsDir() bool { return d.typ.IsDir() } +func (d *unixDirent) Type() FileMode { return d.typ } + +func (d *unixDirent) Info() (FileInfo, error) { + if d.info != nil { + return d.info, nil + } + return lstat(d.parent + "/" + d.name) +} + +func (d *unixDirent) String() string { + return fs.FormatDirEntry(d) +} + +func newUnixDirent(parent, name string, typ FileMode) (DirEntry, error) { + ude := &unixDirent{ + parent: parent, + name: name, + typ: typ, + } + if typ != ^FileMode(0) && !testingForceReadDirLstat { + return ude, nil + } + + info, err := lstat(parent + "/" + name) + if err != nil { + return nil, err + } + + ude.typ = info.Mode().Type() + ude.info = info + return ude, nil +} diff --git a/src/os/file_wasip1.go b/src/os/file_wasip1.go new file mode 100644 index 0000000..c9b05b3 --- /dev/null +++ b/src/os/file_wasip1.go @@ -0,0 +1,22 @@ +// 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. + +//go:build wasip1 + +package os + +import "internal/poll" + +// PollFD returns the poll.FD of the file. +// +// Other packages in std that also import internal/poll (such as net) +// can use a type assertion to access this extension method so that +// they can pass the *poll.FD to functions like poll.Splice. +// +// There is an equivalent function in net.rawConn. +// +// PollFD is not intended for use outside the standard library. +func (f *file) PollFD() *poll.FD { + return &f.pfd +} diff --git a/src/os/file_windows.go b/src/os/file_windows.go new file mode 100644 index 0000000..8d77a63 --- /dev/null +++ b/src/os/file_windows.go @@ -0,0 +1,524 @@ +// Copyright 2009 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 os + +import ( + "errors" + "internal/poll" + "internal/syscall/windows" + "runtime" + "sync" + "syscall" + "unsafe" +) + +// This matches the value in syscall/syscall_windows.go. +const _UTIME_OMIT = -1 + +// file is the real representation of *File. +// The extra level of indirection ensures that no clients of os +// can overwrite this data, which could cause the finalizer +// to close the wrong file descriptor. +type file struct { + pfd poll.FD + name string + dirinfo *dirInfo // nil unless directory being read + appendMode bool // whether file is opened for appending +} + +// Fd returns the Windows handle referencing the open file. +// If f is closed, the file descriptor becomes invalid. +// If f is garbage collected, a finalizer may close the file descriptor, +// making it invalid; see runtime.SetFinalizer for more information on when +// a finalizer might be run. On Unix systems this will cause the SetDeadline +// methods to stop working. +func (file *File) Fd() uintptr { + if file == nil { + return uintptr(syscall.InvalidHandle) + } + return uintptr(file.pfd.Sysfd) +} + +// newFile returns a new File with the given file handle and name. +// Unlike NewFile, it does not check that h is syscall.InvalidHandle. +func newFile(h syscall.Handle, name string, kind string) *File { + if kind == "file" { + var m uint32 + if syscall.GetConsoleMode(h, &m) == nil { + kind = "console" + } + if t, err := syscall.GetFileType(h); err == nil && t == syscall.FILE_TYPE_PIPE { + kind = "pipe" + } + } + + f := &File{&file{ + pfd: poll.FD{ + Sysfd: h, + IsStream: true, + ZeroReadIsEOF: true, + }, + name: name, + }} + runtime.SetFinalizer(f.file, (*file).close) + + // Ignore initialization errors. + // Assume any problems will show up in later I/O. + f.pfd.Init(kind, false) + + return f +} + +// newConsoleFile creates new File that will be used as console. +func newConsoleFile(h syscall.Handle, name string) *File { + return newFile(h, name, "console") +} + +// NewFile returns a new File with the given file descriptor and +// name. The returned value will be nil if fd is not a valid file +// descriptor. +func NewFile(fd uintptr, name string) *File { + h := syscall.Handle(fd) + if h == syscall.InvalidHandle { + return nil + } + return newFile(h, name, "file") +} + +// Auxiliary information if the File describes a directory +type dirInfo struct { + h syscall.Handle // search handle created with FindFirstFile + data syscall.Win32finddata + path string + isempty bool // set if FindFirstFile returns ERROR_FILE_NOT_FOUND +} + +func (d *dirInfo) close() error { + return syscall.FindClose(d.h) +} + +func epipecheck(file *File, e error) { +} + +// DevNull is the name of the operating system's “null device.” +// On Unix-like systems, it is "/dev/null"; on Windows, "NUL". +const DevNull = "NUL" + +func openDir(name string) (d *dirInfo, e error) { + var mask string + + path := fixLongPath(name) + + if len(path) == 2 && path[1] == ':' { // it is a drive letter, like C: + mask = path + `*` + } else if len(path) > 0 { + lc := path[len(path)-1] + if lc == '/' || lc == '\\' { + mask = path + `*` + } else { + mask = path + `\*` + } + } else { + mask = `\*` + } + maskp, e := syscall.UTF16PtrFromString(mask) + if e != nil { + return nil, e + } + d = new(dirInfo) + d.h, e = syscall.FindFirstFile(maskp, &d.data) + if e != nil { + // FindFirstFile returns ERROR_FILE_NOT_FOUND when + // no matching files can be found. Then, if directory + // exists, we should proceed. + // If FindFirstFile failed because name does not point + // to a directory, we should return ENOTDIR. + var fa syscall.Win32FileAttributeData + pathp, e1 := syscall.UTF16PtrFromString(path) + if e1 != nil { + return nil, e + } + e1 = syscall.GetFileAttributesEx(pathp, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa))) + if e1 != nil { + return nil, e + } + if fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 { + return nil, syscall.ENOTDIR + } + if e != syscall.ERROR_FILE_NOT_FOUND { + return nil, e + } + d.isempty = true + } + d.path = path + if !isAbs(d.path) { + d.path, e = syscall.FullPath(d.path) + if e != nil { + d.close() + return nil, e + } + } + return d, nil +} + +// openFileNolog is the Windows implementation of OpenFile. +func openFileNolog(name string, flag int, perm FileMode) (*File, error) { + if name == "" { + return nil, &PathError{Op: "open", Path: name, Err: syscall.ENOENT} + } + path := fixLongPath(name) + r, e := syscall.Open(path, flag|syscall.O_CLOEXEC, syscallMode(perm)) + if e != nil { + // We should return EISDIR when we are trying to open a directory with write access. + if e == syscall.ERROR_ACCESS_DENIED && (flag&O_WRONLY != 0 || flag&O_RDWR != 0) { + pathp, e1 := syscall.UTF16PtrFromString(path) + if e1 == nil { + var fa syscall.Win32FileAttributeData + e1 = syscall.GetFileAttributesEx(pathp, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa))) + if e1 == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { + e = syscall.EISDIR + } + } + } + return nil, &PathError{Op: "open", Path: name, Err: e} + } + f, e := newFile(r, name, "file"), nil + if e != nil { + return nil, &PathError{Op: "open", Path: name, Err: e} + } + return f, nil +} + +func (file *file) close() error { + if file == nil { + return syscall.EINVAL + } + if file.dirinfo != nil { + file.dirinfo.close() + file.dirinfo = nil + } + var err error + if e := file.pfd.Close(); e != nil { + if e == poll.ErrFileClosing { + e = ErrClosed + } + err = &PathError{Op: "close", Path: file.name, Err: e} + } + + // no need for a finalizer anymore + runtime.SetFinalizer(file, nil) + return err +} + +// seek sets the offset for the next Read or Write on file to offset, interpreted +// according to whence: 0 means relative to the origin of the file, 1 means +// relative to the current offset, and 2 means relative to the end. +// It returns the new offset and an error, if any. +func (f *File) seek(offset int64, whence int) (ret int64, err error) { + if f.dirinfo != nil { + // Free cached dirinfo, so we allocate a new one if we + // access this file as a directory again. See #35767 and #37161. + f.dirinfo.close() + f.dirinfo = nil + } + ret, err = f.pfd.Seek(offset, whence) + runtime.KeepAlive(f) + return ret, err +} + +// Truncate changes the size of the named file. +// If the file is a symbolic link, it changes the size of the link's target. +func Truncate(name string, size int64) error { + f, e := OpenFile(name, O_WRONLY, 0666) + if e != nil { + return e + } + defer f.Close() + e1 := f.Truncate(size) + if e1 != nil { + return e1 + } + return nil +} + +// Remove removes the named file or directory. +// If there is an error, it will be of type *PathError. +func Remove(name string) error { + p, e := syscall.UTF16PtrFromString(fixLongPath(name)) + if e != nil { + return &PathError{Op: "remove", Path: name, Err: e} + } + + // Go file interface forces us to know whether + // name is a file or directory. Try both. + e = syscall.DeleteFile(p) + if e == nil { + return nil + } + e1 := syscall.RemoveDirectory(p) + if e1 == nil { + return nil + } + + // Both failed: figure out which error to return. + if e1 != e { + a, e2 := syscall.GetFileAttributes(p) + if e2 != nil { + e = e2 + } else { + if a&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { + e = e1 + } else if a&syscall.FILE_ATTRIBUTE_READONLY != 0 { + if e1 = syscall.SetFileAttributes(p, a&^syscall.FILE_ATTRIBUTE_READONLY); e1 == nil { + if e = syscall.DeleteFile(p); e == nil { + return nil + } + } + } + } + } + return &PathError{Op: "remove", Path: name, Err: e} +} + +func rename(oldname, newname string) error { + e := windows.Rename(fixLongPath(oldname), fixLongPath(newname)) + if e != nil { + return &LinkError{"rename", oldname, newname, e} + } + return nil +} + +// Pipe returns a connected pair of Files; reads from r return bytes written to w. +// It returns the files and an error, if any. The Windows handles underlying +// the returned files are marked as inheritable by child processes. +func Pipe() (r *File, w *File, err error) { + var p [2]syscall.Handle + e := syscall.Pipe(p[:]) + if e != nil { + return nil, nil, NewSyscallError("pipe", e) + } + return newFile(p[0], "|0", "pipe"), newFile(p[1], "|1", "pipe"), nil +} + +var ( + useGetTempPath2Once sync.Once + useGetTempPath2 bool +) + +func tempDir() string { + useGetTempPath2Once.Do(func() { + useGetTempPath2 = (windows.ErrorLoadingGetTempPath2() == nil) + }) + getTempPath := syscall.GetTempPath + if useGetTempPath2 { + getTempPath = windows.GetTempPath2 + } + n := uint32(syscall.MAX_PATH) + for { + b := make([]uint16, n) + n, _ = getTempPath(uint32(len(b)), &b[0]) + if n > uint32(len(b)) { + continue + } + if n == 3 && b[1] == ':' && b[2] == '\\' { + // Do nothing for path, like C:\. + } else if n > 0 && b[n-1] == '\\' { + // Otherwise remove terminating \. + n-- + } + return syscall.UTF16ToString(b[:n]) + } +} + +// Link creates newname as a hard link to the oldname file. +// If there is an error, it will be of type *LinkError. +func Link(oldname, newname string) error { + n, err := syscall.UTF16PtrFromString(fixLongPath(newname)) + if err != nil { + return &LinkError{"link", oldname, newname, err} + } + o, err := syscall.UTF16PtrFromString(fixLongPath(oldname)) + if err != nil { + return &LinkError{"link", oldname, newname, err} + } + err = syscall.CreateHardLink(n, o, 0) + if err != nil { + return &LinkError{"link", oldname, newname, err} + } + return nil +} + +// Symlink creates newname as a symbolic link to oldname. +// On Windows, a symlink to a non-existent oldname creates a file symlink; +// if oldname is later created as a directory the symlink will not work. +// If there is an error, it will be of type *LinkError. +func Symlink(oldname, newname string) error { + // '/' does not work in link's content + oldname = fromSlash(oldname) + + // need the exact location of the oldname when it's relative to determine if it's a directory + destpath := oldname + if v := volumeName(oldname); v == "" { + if len(oldname) > 0 && IsPathSeparator(oldname[0]) { + // oldname is relative to the volume containing newname. + if v = volumeName(newname); v != "" { + // Prepend the volume explicitly, because it may be different from the + // volume of the current working directory. + destpath = v + oldname + } + } else { + // oldname is relative to newname. + destpath = dirname(newname) + `\` + oldname + } + } + + fi, err := Stat(destpath) + isdir := err == nil && fi.IsDir() + + n, err := syscall.UTF16PtrFromString(fixLongPath(newname)) + if err != nil { + return &LinkError{"symlink", oldname, newname, err} + } + o, err := syscall.UTF16PtrFromString(fixLongPath(oldname)) + if err != nil { + return &LinkError{"symlink", oldname, newname, err} + } + + var flags uint32 = windows.SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE + if isdir { + flags |= syscall.SYMBOLIC_LINK_FLAG_DIRECTORY + } + err = syscall.CreateSymbolicLink(n, o, flags) + if err != nil { + // the unprivileged create flag is unsupported + // below Windows 10 (1703, v10.0.14972). retry without it. + flags &^= windows.SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE + err = syscall.CreateSymbolicLink(n, o, flags) + if err != nil { + return &LinkError{"symlink", oldname, newname, err} + } + } + return nil +} + +// openSymlink calls CreateFile Windows API with FILE_FLAG_OPEN_REPARSE_POINT +// parameter, so that Windows does not follow symlink, if path is a symlink. +// openSymlink returns opened file handle. +func openSymlink(path string) (syscall.Handle, error) { + p, err := syscall.UTF16PtrFromString(path) + if err != nil { + return 0, err + } + attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS) + // Use FILE_FLAG_OPEN_REPARSE_POINT, otherwise CreateFile will follow symlink. + // See https://docs.microsoft.com/en-us/windows/desktop/FileIO/symbolic-link-effects-on-file-systems-functions#createfile-and-createfiletransacted + attrs |= syscall.FILE_FLAG_OPEN_REPARSE_POINT + h, err := syscall.CreateFile(p, 0, 0, nil, syscall.OPEN_EXISTING, attrs, 0) + if err != nil { + return 0, err + } + return h, nil +} + +// normaliseLinkPath converts absolute paths returned by +// DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, ...) +// into paths acceptable by all Windows APIs. +// For example, it converts +// +// \??\C:\foo\bar into C:\foo\bar +// \??\UNC\foo\bar into \\foo\bar +// \??\Volume{abc}\ into C:\ +func normaliseLinkPath(path string) (string, error) { + if len(path) < 4 || path[:4] != `\??\` { + // unexpected path, return it as is + return path, nil + } + // we have path that start with \??\ + s := path[4:] + switch { + case len(s) >= 2 && s[1] == ':': // \??\C:\foo\bar + return s, nil + case len(s) >= 4 && s[:4] == `UNC\`: // \??\UNC\foo\bar + return `\\` + s[4:], nil + } + + // handle paths, like \??\Volume{abc}\... + + err := windows.LoadGetFinalPathNameByHandle() + if err != nil { + // we must be using old version of Windows + return "", err + } + + h, err := openSymlink(path) + if err != nil { + return "", err + } + defer syscall.CloseHandle(h) + + buf := make([]uint16, 100) + for { + n, err := windows.GetFinalPathNameByHandle(h, &buf[0], uint32(len(buf)), windows.VOLUME_NAME_DOS) + if err != nil { + return "", err + } + if n < uint32(len(buf)) { + break + } + buf = make([]uint16, n) + } + s = syscall.UTF16ToString(buf) + if len(s) > 4 && s[:4] == `\\?\` { + s = s[4:] + if len(s) > 3 && s[:3] == `UNC` { + // return path like \\server\share\... + return `\` + s[3:], nil + } + return s, nil + } + return "", errors.New("GetFinalPathNameByHandle returned unexpected path: " + s) +} + +func readlink(path string) (string, error) { + h, err := openSymlink(path) + if err != nil { + return "", err + } + defer syscall.CloseHandle(h) + + rdbbuf := make([]byte, syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE) + var bytesReturned uint32 + err = syscall.DeviceIoControl(h, syscall.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil) + if err != nil { + return "", err + } + + rdb := (*windows.REPARSE_DATA_BUFFER)(unsafe.Pointer(&rdbbuf[0])) + switch rdb.ReparseTag { + case syscall.IO_REPARSE_TAG_SYMLINK: + rb := (*windows.SymbolicLinkReparseBuffer)(unsafe.Pointer(&rdb.DUMMYUNIONNAME)) + s := rb.Path() + if rb.Flags&windows.SYMLINK_FLAG_RELATIVE != 0 { + return s, nil + } + return normaliseLinkPath(s) + case windows.IO_REPARSE_TAG_MOUNT_POINT: + return normaliseLinkPath((*windows.MountPointReparseBuffer)(unsafe.Pointer(&rdb.DUMMYUNIONNAME)).Path()) + default: + // the path is not a symlink or junction but another type of reparse + // point + return "", syscall.ENOENT + } +} + +// Readlink returns the destination of the named symbolic link. +// If there is an error, it will be of type *PathError. +func Readlink(name string) (string, error) { + s, err := readlink(fixLongPath(name)) + if err != nil { + return "", &PathError{Op: "readlink", Path: name, Err: err} + } + return s, nil +} diff --git a/src/os/getwd.go b/src/os/getwd.go new file mode 100644 index 0000000..90604cf --- /dev/null +++ b/src/os/getwd.go @@ -0,0 +1,126 @@ +// Copyright 2009 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 os + +import ( + "runtime" + "sync" + "syscall" +) + +var getwdCache struct { + sync.Mutex + dir string +} + +// Getwd returns a rooted path name corresponding to the +// current directory. If the current directory can be +// reached via multiple paths (due to symbolic links), +// Getwd may return any one of them. +func Getwd() (dir string, err error) { + if runtime.GOOS == "windows" || runtime.GOOS == "plan9" { + return syscall.Getwd() + } + + // Clumsy but widespread kludge: + // if $PWD is set and matches ".", use it. + dot, err := statNolog(".") + if err != nil { + return "", err + } + dir = Getenv("PWD") + if len(dir) > 0 && dir[0] == '/' { + d, err := statNolog(dir) + if err == nil && SameFile(dot, d) { + return dir, nil + } + } + + // If the operating system provides a Getwd call, use it. + // Otherwise, we're trying to find our way back to ".". + if syscall.ImplementsGetwd { + var ( + s string + e error + ) + for { + s, e = syscall.Getwd() + if e != syscall.EINTR { + break + } + } + return s, NewSyscallError("getwd", e) + } + + // Apply same kludge but to cached dir instead of $PWD. + getwdCache.Lock() + dir = getwdCache.dir + getwdCache.Unlock() + if len(dir) > 0 { + d, err := statNolog(dir) + if err == nil && SameFile(dot, d) { + return dir, nil + } + } + + // Root is a special case because it has no parent + // and ends in a slash. + root, err := statNolog("/") + if err != nil { + // Can't stat root - no hope. + return "", err + } + if SameFile(root, dot) { + return "/", nil + } + + // General algorithm: find name in parent + // and then find name of parent. Each iteration + // adds /name to the beginning of dir. + dir = "" + for parent := ".."; ; parent = "../" + parent { + if len(parent) >= 1024 { // Sanity check + return "", syscall.ENAMETOOLONG + } + fd, err := openFileNolog(parent, O_RDONLY, 0) + if err != nil { + return "", err + } + + for { + names, err := fd.Readdirnames(100) + if err != nil { + fd.Close() + return "", err + } + for _, name := range names { + d, _ := lstatNolog(parent + "/" + name) + if SameFile(d, dot) { + dir = "/" + name + dir + goto Found + } + } + } + + Found: + pd, err := fd.Stat() + fd.Close() + if err != nil { + return "", err + } + if SameFile(pd, root) { + break + } + // Set up for next round. + dot = pd + } + + // Save answer as hint to avoid the expensive path next time. + getwdCache.Lock() + getwdCache.dir = dir + getwdCache.Unlock() + + return dir, nil +} diff --git a/src/os/os_test.go b/src/os/os_test.go new file mode 100644 index 0000000..94c3ad0 --- /dev/null +++ b/src/os/os_test.go @@ -0,0 +1,3272 @@ +// Copyright 2009 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 os_test + +import ( + "errors" + "flag" + "fmt" + "internal/testenv" + "io" + "io/fs" + . "os" + "os/exec" + "path/filepath" + "reflect" + "runtime" + "runtime/debug" + "sort" + "strings" + "sync" + "syscall" + "testing" + "testing/fstest" + "time" +) + +func TestMain(m *testing.M) { + if Getenv("GO_OS_TEST_DRAIN_STDIN") == "1" { + Stdout.Close() + io.Copy(io.Discard, Stdin) + Exit(0) + } + + Exit(m.Run()) +} + +var dot = []string{ + "dir_unix.go", + "env.go", + "error.go", + "file.go", + "os_test.go", + "types.go", + "stat_darwin.go", + "stat_linux.go", +} + +type sysDir struct { + name string + files []string +} + +var sysdir = func() *sysDir { + switch runtime.GOOS { + case "android": + return &sysDir{ + "/system/lib", + []string{ + "libmedia.so", + "libpowermanager.so", + }, + } + case "ios": + wd, err := syscall.Getwd() + if err != nil { + wd = err.Error() + } + sd := &sysDir{ + filepath.Join(wd, "..", ".."), + []string{ + "ResourceRules.plist", + "Info.plist", + }, + } + found := true + for _, f := range sd.files { + path := filepath.Join(sd.name, f) + if _, err := Stat(path); err != nil { + found = false + break + } + } + if found { + return sd + } + // In a self-hosted iOS build the above files might + // not exist. Look for system files instead below. + case "windows": + return &sysDir{ + Getenv("SystemRoot") + "\\system32\\drivers\\etc", + []string{ + "networks", + "protocol", + "services", + }, + } + case "plan9": + return &sysDir{ + "/lib/ndb", + []string{ + "common", + "local", + }, + } + case "wasip1": + // wasmtime has issues resolving symbolic links that are often present + // in directories like /etc/group below (e.g. private/etc/group on OSX). + // For this reason we use files in the Go source tree instead. + return &sysDir{ + runtime.GOROOT(), + []string{ + "go.env", + "LICENSE", + "CONTRIBUTING.md", + }, + } + } + return &sysDir{ + "/etc", + []string{ + "group", + "hosts", + "passwd", + }, + } +}() + +func size(name string, t *testing.T) int64 { + file, err := Open(name) + if err != nil { + t.Fatal("open failed:", err) + } + defer func() { + if err := file.Close(); err != nil { + t.Error(err) + } + }() + n, err := io.Copy(io.Discard, file) + if err != nil { + t.Fatal(err) + } + return n +} + +func equal(name1, name2 string) (r bool) { + switch runtime.GOOS { + case "windows": + r = strings.ToLower(name1) == strings.ToLower(name2) + default: + r = name1 == name2 + } + return +} + +// localTmp returns a local temporary directory not on NFS. +func localTmp() string { + switch runtime.GOOS { + case "android", "ios", "windows": + return TempDir() + } + return "/tmp" +} + +func newFile(testName string, t *testing.T) (f *File) { + f, err := CreateTemp(localTmp(), "_Go_"+testName) + if err != nil { + t.Fatalf("TempFile %s: %s", testName, err) + } + return +} + +func newDir(testName string, t *testing.T) (name string) { + name, err := MkdirTemp(localTmp(), "_Go_"+testName) + if err != nil { + t.Fatalf("TempDir %s: %s", testName, err) + } + return +} + +var sfdir = sysdir.name +var sfname = sysdir.files[0] + +func TestStat(t *testing.T) { + t.Parallel() + + path := sfdir + "/" + sfname + dir, err := Stat(path) + if err != nil { + t.Fatal("stat failed:", err) + } + if !equal(sfname, dir.Name()) { + t.Error("name should be ", sfname, "; is", dir.Name()) + } + filesize := size(path, t) + if dir.Size() != filesize { + t.Error("size should be", filesize, "; is", dir.Size()) + } +} + +func TestStatError(t *testing.T) { + defer chtmpdir(t)() + + path := "no-such-file" + + fi, err := Stat(path) + if err == nil { + t.Fatal("got nil, want error") + } + if fi != nil { + t.Errorf("got %v, want nil", fi) + } + if perr, ok := err.(*PathError); !ok { + t.Errorf("got %T, want %T", err, perr) + } + + testenv.MustHaveSymlink(t) + + link := "symlink" + err = Symlink(path, link) + if err != nil { + t.Fatal(err) + } + + fi, err = Stat(link) + if err == nil { + t.Fatal("got nil, want error") + } + if fi != nil { + t.Errorf("got %v, want nil", fi) + } + if perr, ok := err.(*PathError); !ok { + t.Errorf("got %T, want %T", err, perr) + } +} + +func TestStatSymlinkLoop(t *testing.T) { + testenv.MustHaveSymlink(t) + + defer chtmpdir(t)() + + err := Symlink("x", "y") + if err != nil { + t.Fatal(err) + } + defer Remove("y") + + err = Symlink("y", "x") + if err != nil { + t.Fatal(err) + } + defer Remove("x") + + _, err = Stat("x") + if _, ok := err.(*fs.PathError); !ok { + t.Errorf("expected *PathError, got %T: %v\n", err, err) + } +} + +func TestFstat(t *testing.T) { + t.Parallel() + + path := sfdir + "/" + sfname + file, err1 := Open(path) + if err1 != nil { + t.Fatal("open failed:", err1) + } + defer file.Close() + dir, err2 := file.Stat() + if err2 != nil { + t.Fatal("fstat failed:", err2) + } + if !equal(sfname, dir.Name()) { + t.Error("name should be ", sfname, "; is", dir.Name()) + } + filesize := size(path, t) + if dir.Size() != filesize { + t.Error("size should be", filesize, "; is", dir.Size()) + } +} + +func TestLstat(t *testing.T) { + t.Parallel() + + path := sfdir + "/" + sfname + dir, err := Lstat(path) + if err != nil { + t.Fatal("lstat failed:", err) + } + if !equal(sfname, dir.Name()) { + t.Error("name should be ", sfname, "; is", dir.Name()) + } + if dir.Mode()&ModeSymlink == 0 { + filesize := size(path, t) + if dir.Size() != filesize { + t.Error("size should be", filesize, "; is", dir.Size()) + } + } +} + +// Read with length 0 should not return EOF. +func TestRead0(t *testing.T) { + t.Parallel() + + path := sfdir + "/" + sfname + f, err := Open(path) + if err != nil { + t.Fatal("open failed:", err) + } + defer f.Close() + + b := make([]byte, 0) + n, err := f.Read(b) + if n != 0 || err != nil { + t.Errorf("Read(0) = %d, %v, want 0, nil", n, err) + } + b = make([]byte, 100) + n, err = f.Read(b) + if n <= 0 || err != nil { + t.Errorf("Read(100) = %d, %v, want >0, nil", n, err) + } +} + +// Reading a closed file should return ErrClosed error +func TestReadClosed(t *testing.T) { + t.Parallel() + + path := sfdir + "/" + sfname + file, err := Open(path) + if err != nil { + t.Fatal("open failed:", err) + } + file.Close() // close immediately + + b := make([]byte, 100) + _, err = file.Read(b) + + e, ok := err.(*PathError) + if !ok || e.Err != ErrClosed { + t.Fatalf("Read: got %T(%v), want %T(%v)", err, err, e, ErrClosed) + } +} + +func testReaddirnames(dir string, contents []string) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + + file, err := Open(dir) + if err != nil { + t.Fatalf("open %q failed: %v", dir, err) + } + defer file.Close() + s, err2 := file.Readdirnames(-1) + if err2 != nil { + t.Fatalf("Readdirnames %q failed: %v", dir, err2) + } + for _, m := range contents { + found := false + for _, n := range s { + if n == "." || n == ".." { + t.Errorf("got %q in directory", n) + } + if !equal(m, n) { + continue + } + if found { + t.Error("present twice:", m) + } + found = true + } + if !found { + t.Error("could not find", m) + } + } + if s == nil { + t.Error("Readdirnames returned nil instead of empty slice") + } + } +} + +func testReaddir(dir string, contents []string) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + + file, err := Open(dir) + if err != nil { + t.Fatalf("open %q failed: %v", dir, err) + } + defer file.Close() + s, err2 := file.Readdir(-1) + if err2 != nil { + t.Fatalf("Readdir %q failed: %v", dir, err2) + } + for _, m := range contents { + found := false + for _, n := range s { + if n.Name() == "." || n.Name() == ".." { + t.Errorf("got %q in directory", n.Name()) + } + if !equal(m, n.Name()) { + continue + } + if found { + t.Error("present twice:", m) + } + found = true + } + if !found { + t.Error("could not find", m) + } + } + if s == nil { + t.Error("Readdir returned nil instead of empty slice") + } + } +} + +func testReadDir(dir string, contents []string) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + + file, err := Open(dir) + if err != nil { + t.Fatalf("open %q failed: %v", dir, err) + } + defer file.Close() + s, err2 := file.ReadDir(-1) + if err2 != nil { + t.Fatalf("ReadDir %q failed: %v", dir, err2) + } + for _, m := range contents { + found := false + for _, n := range s { + if n.Name() == "." || n.Name() == ".." { + t.Errorf("got %q in directory", n) + } + if !equal(m, n.Name()) { + continue + } + if found { + t.Error("present twice:", m) + } + found = true + lstat, err := Lstat(dir + "/" + m) + if err != nil { + t.Fatal(err) + } + if n.IsDir() != lstat.IsDir() { + t.Errorf("%s: IsDir=%v, want %v", m, n.IsDir(), lstat.IsDir()) + } + if n.Type() != lstat.Mode().Type() { + t.Errorf("%s: IsDir=%v, want %v", m, n.Type(), lstat.Mode().Type()) + } + info, err := n.Info() + if err != nil { + t.Errorf("%s: Info: %v", m, err) + continue + } + if !SameFile(info, lstat) { + t.Errorf("%s: Info: SameFile(info, lstat) = false", m) + } + } + if !found { + t.Error("could not find", m) + } + } + if s == nil { + t.Error("ReadDir returned nil instead of empty slice") + } + } +} + +func TestFileReaddirnames(t *testing.T) { + t.Parallel() + + t.Run(".", testReaddirnames(".", dot)) + t.Run("sysdir", testReaddirnames(sysdir.name, sysdir.files)) + t.Run("TempDir", testReaddirnames(t.TempDir(), nil)) +} + +func TestFileReaddir(t *testing.T) { + t.Parallel() + + t.Run(".", testReaddir(".", dot)) + t.Run("sysdir", testReaddir(sysdir.name, sysdir.files)) + t.Run("TempDir", testReaddir(t.TempDir(), nil)) +} + +func TestFileReadDir(t *testing.T) { + t.Parallel() + + t.Run(".", testReadDir(".", dot)) + t.Run("sysdir", testReadDir(sysdir.name, sysdir.files)) + t.Run("TempDir", testReadDir(t.TempDir(), nil)) +} + +func benchmarkReaddirname(path string, b *testing.B) { + var nentries int + for i := 0; i < b.N; i++ { + f, err := Open(path) + if err != nil { + b.Fatalf("open %q failed: %v", path, err) + } + ns, err := f.Readdirnames(-1) + f.Close() + if err != nil { + b.Fatalf("readdirnames %q failed: %v", path, err) + } + nentries = len(ns) + } + b.Logf("benchmarkReaddirname %q: %d entries", path, nentries) +} + +func benchmarkReaddir(path string, b *testing.B) { + var nentries int + for i := 0; i < b.N; i++ { + f, err := Open(path) + if err != nil { + b.Fatalf("open %q failed: %v", path, err) + } + fs, err := f.Readdir(-1) + f.Close() + if err != nil { + b.Fatalf("readdir %q failed: %v", path, err) + } + nentries = len(fs) + } + b.Logf("benchmarkReaddir %q: %d entries", path, nentries) +} + +func benchmarkReadDir(path string, b *testing.B) { + var nentries int + for i := 0; i < b.N; i++ { + f, err := Open(path) + if err != nil { + b.Fatalf("open %q failed: %v", path, err) + } + fs, err := f.ReadDir(-1) + f.Close() + if err != nil { + b.Fatalf("readdir %q failed: %v", path, err) + } + nentries = len(fs) + } + b.Logf("benchmarkReadDir %q: %d entries", path, nentries) +} + +func BenchmarkReaddirname(b *testing.B) { + benchmarkReaddirname(".", b) +} + +func BenchmarkReaddir(b *testing.B) { + benchmarkReaddir(".", b) +} + +func BenchmarkReadDir(b *testing.B) { + benchmarkReadDir(".", b) +} + +func benchmarkStat(b *testing.B, path string) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Stat(path) + if err != nil { + b.Fatalf("Stat(%q) failed: %v", path, err) + } + } +} + +func benchmarkLstat(b *testing.B, path string) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Lstat(path) + if err != nil { + b.Fatalf("Lstat(%q) failed: %v", path, err) + } + } +} + +func BenchmarkStatDot(b *testing.B) { + benchmarkStat(b, ".") +} + +func BenchmarkStatFile(b *testing.B) { + benchmarkStat(b, filepath.Join(runtime.GOROOT(), "src/os/os_test.go")) +} + +func BenchmarkStatDir(b *testing.B) { + benchmarkStat(b, filepath.Join(runtime.GOROOT(), "src/os")) +} + +func BenchmarkLstatDot(b *testing.B) { + benchmarkLstat(b, ".") +} + +func BenchmarkLstatFile(b *testing.B) { + benchmarkLstat(b, filepath.Join(runtime.GOROOT(), "src/os/os_test.go")) +} + +func BenchmarkLstatDir(b *testing.B) { + benchmarkLstat(b, filepath.Join(runtime.GOROOT(), "src/os")) +} + +// Read the directory one entry at a time. +func smallReaddirnames(file *File, length int, t *testing.T) []string { + names := make([]string, length) + count := 0 + for { + d, err := file.Readdirnames(1) + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("readdirnames %q failed: %v", file.Name(), err) + } + if len(d) == 0 { + t.Fatalf("readdirnames %q returned empty slice and no error", file.Name()) + } + names[count] = d[0] + count++ + } + return names[0:count] +} + +// Check that reading a directory one entry at a time gives the same result +// as reading it all at once. +func TestReaddirnamesOneAtATime(t *testing.T) { + t.Parallel() + + // big directory that doesn't change often. + dir := "/usr/bin" + switch runtime.GOOS { + case "android": + dir = "/system/bin" + case "ios", "wasip1": + wd, err := Getwd() + if err != nil { + t.Fatal(err) + } + dir = wd + case "plan9": + dir = "/bin" + case "windows": + dir = Getenv("SystemRoot") + "\\system32" + } + file, err := Open(dir) + if err != nil { + t.Fatalf("open %q failed: %v", dir, err) + } + defer file.Close() + all, err1 := file.Readdirnames(-1) + if err1 != nil { + t.Fatalf("readdirnames %q failed: %v", dir, err1) + } + file1, err2 := Open(dir) + if err2 != nil { + t.Fatalf("open %q failed: %v", dir, err2) + } + defer file1.Close() + small := smallReaddirnames(file1, len(all)+100, t) // +100 in case we screw up + if len(small) < len(all) { + t.Fatalf("len(small) is %d, less than %d", len(small), len(all)) + } + for i, n := range all { + if small[i] != n { + t.Errorf("small read %q mismatch: %v", small[i], n) + } + } +} + +func TestReaddirNValues(t *testing.T) { + if testing.Short() { + t.Skip("test.short; skipping") + } + t.Parallel() + + dir := t.TempDir() + for i := 1; i <= 105; i++ { + f, err := Create(filepath.Join(dir, fmt.Sprintf("%d", i))) + if err != nil { + t.Fatalf("Create: %v", err) + } + f.Write([]byte(strings.Repeat("X", i))) + f.Close() + } + + var d *File + openDir := func() { + var err error + d, err = Open(dir) + if err != nil { + t.Fatalf("Open directory: %v", err) + } + } + + readdirExpect := func(n, want int, wantErr error) { + t.Helper() + fi, err := d.Readdir(n) + if err != wantErr { + t.Fatalf("Readdir of %d got error %v, want %v", n, err, wantErr) + } + if g, e := len(fi), want; g != e { + t.Errorf("Readdir of %d got %d files, want %d", n, g, e) + } + } + + readDirExpect := func(n, want int, wantErr error) { + t.Helper() + de, err := d.ReadDir(n) + if err != wantErr { + t.Fatalf("ReadDir of %d got error %v, want %v", n, err, wantErr) + } + if g, e := len(de), want; g != e { + t.Errorf("ReadDir of %d got %d files, want %d", n, g, e) + } + } + + readdirnamesExpect := func(n, want int, wantErr error) { + t.Helper() + fi, err := d.Readdirnames(n) + if err != wantErr { + t.Fatalf("Readdirnames of %d got error %v, want %v", n, err, wantErr) + } + if g, e := len(fi), want; g != e { + t.Errorf("Readdirnames of %d got %d files, want %d", n, g, e) + } + } + + for _, fn := range []func(int, int, error){readdirExpect, readdirnamesExpect, readDirExpect} { + // Test the slurp case + openDir() + fn(0, 105, nil) + fn(0, 0, nil) + d.Close() + + // Slurp with -1 instead + openDir() + fn(-1, 105, nil) + fn(-2, 0, nil) + fn(0, 0, nil) + d.Close() + + // Test the bounded case + openDir() + fn(1, 1, nil) + fn(2, 2, nil) + fn(105, 102, nil) // and tests buffer >100 case + fn(3, 0, io.EOF) + d.Close() + } +} + +func touch(t *testing.T, name string) { + f, err := Create(name) + if err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } +} + +func TestReaddirStatFailures(t *testing.T) { + switch runtime.GOOS { + case "windows", "plan9": + // Windows and Plan 9 already do this correctly, + // but are structured with different syscalls such + // that they don't use Lstat, so the hook below for + // testing it wouldn't work. + t.Skipf("skipping test on %v", runtime.GOOS) + } + + var xerr error // error to return for x + *LstatP = func(path string) (FileInfo, error) { + if xerr != nil && strings.HasSuffix(path, "x") { + return nil, xerr + } + return Lstat(path) + } + defer func() { *LstatP = Lstat }() + + dir := t.TempDir() + touch(t, filepath.Join(dir, "good1")) + touch(t, filepath.Join(dir, "x")) // will disappear or have an error + touch(t, filepath.Join(dir, "good2")) + readDir := func() ([]FileInfo, error) { + d, err := Open(dir) + if err != nil { + t.Fatal(err) + } + defer d.Close() + return d.Readdir(-1) + } + mustReadDir := func(testName string) []FileInfo { + fis, err := readDir() + if err != nil { + t.Fatalf("%s: Readdir: %v", testName, err) + } + return fis + } + names := func(fis []FileInfo) []string { + s := make([]string, len(fis)) + for i, fi := range fis { + s[i] = fi.Name() + } + sort.Strings(s) + return s + } + + if got, want := names(mustReadDir("initial readdir")), + []string{"good1", "good2", "x"}; !reflect.DeepEqual(got, want) { + t.Errorf("initial readdir got %q; want %q", got, want) + } + + xerr = ErrNotExist + if got, want := names(mustReadDir("with x disappearing")), + []string{"good1", "good2"}; !reflect.DeepEqual(got, want) { + t.Errorf("with x disappearing, got %q; want %q", got, want) + } + + xerr = errors.New("some real error") + if _, err := readDir(); err != xerr { + t.Errorf("with a non-ErrNotExist error, got error %v; want %v", err, xerr) + } +} + +// Readdir on a regular file should fail. +func TestReaddirOfFile(t *testing.T) { + t.Parallel() + + f, err := CreateTemp(t.TempDir(), "_Go_ReaddirOfFile") + if err != nil { + t.Fatal(err) + } + f.Write([]byte("foo")) + f.Close() + reg, err := Open(f.Name()) + if err != nil { + t.Fatal(err) + } + defer reg.Close() + + names, err := reg.Readdirnames(-1) + if err == nil { + t.Error("Readdirnames succeeded; want non-nil error") + } + var pe *PathError + if !errors.As(err, &pe) || pe.Path != f.Name() { + t.Errorf("Readdirnames returned %q; want a PathError with path %q", err, f.Name()) + } + if len(names) > 0 { + t.Errorf("unexpected dir names in regular file: %q", names) + } +} + +func TestHardLink(t *testing.T) { + testenv.MustHaveLink(t) + + defer chtmpdir(t)() + from, to := "hardlinktestfrom", "hardlinktestto" + file, err := Create(to) + if err != nil { + t.Fatalf("open %q failed: %v", to, err) + } + if err = file.Close(); err != nil { + t.Errorf("close %q failed: %v", to, err) + } + err = Link(to, from) + if err != nil { + t.Fatalf("link %q, %q failed: %v", to, from, err) + } + + none := "hardlinktestnone" + err = Link(none, none) + // Check the returned error is well-formed. + if lerr, ok := err.(*LinkError); !ok || lerr.Error() == "" { + t.Errorf("link %q, %q failed to return a valid error", none, none) + } + + tostat, err := Stat(to) + if err != nil { + t.Fatalf("stat %q failed: %v", to, err) + } + fromstat, err := Stat(from) + if err != nil { + t.Fatalf("stat %q failed: %v", from, err) + } + if !SameFile(tostat, fromstat) { + t.Errorf("link %q, %q did not create hard link", to, from) + } + // We should not be able to perform the same Link() a second time + err = Link(to, from) + switch err := err.(type) { + case *LinkError: + if err.Op != "link" { + t.Errorf("Link(%q, %q) err.Op = %q; want %q", to, from, err.Op, "link") + } + if err.Old != to { + t.Errorf("Link(%q, %q) err.Old = %q; want %q", to, from, err.Old, to) + } + if err.New != from { + t.Errorf("Link(%q, %q) err.New = %q; want %q", to, from, err.New, from) + } + if !IsExist(err.Err) { + t.Errorf("Link(%q, %q) err.Err = %q; want %q", to, from, err.Err, "file exists error") + } + case nil: + t.Errorf("link %q, %q: expected error, got nil", from, to) + default: + t.Errorf("link %q, %q: expected %T, got %T %v", from, to, new(LinkError), err, err) + } +} + +// chtmpdir changes the working directory to a new temporary directory and +// provides a cleanup function. +func chtmpdir(t *testing.T) func() { + oldwd, err := Getwd() + if err != nil { + t.Fatalf("chtmpdir: %v", err) + } + d, err := MkdirTemp("", "test") + if err != nil { + t.Fatalf("chtmpdir: %v", err) + } + if err := Chdir(d); err != nil { + t.Fatalf("chtmpdir: %v", err) + } + return func() { + if err := Chdir(oldwd); err != nil { + t.Fatalf("chtmpdir: %v", err) + } + RemoveAll(d) + } +} + +func TestSymlink(t *testing.T) { + testenv.MustHaveSymlink(t) + + defer chtmpdir(t)() + from, to := "symlinktestfrom", "symlinktestto" + file, err := Create(to) + if err != nil { + t.Fatalf("Create(%q) failed: %v", to, err) + } + if err = file.Close(); err != nil { + t.Errorf("Close(%q) failed: %v", to, err) + } + err = Symlink(to, from) + if err != nil { + t.Fatalf("Symlink(%q, %q) failed: %v", to, from, err) + } + tostat, err := Lstat(to) + if err != nil { + t.Fatalf("Lstat(%q) failed: %v", to, err) + } + if tostat.Mode()&ModeSymlink != 0 { + t.Fatalf("Lstat(%q).Mode()&ModeSymlink = %v, want 0", to, tostat.Mode()&ModeSymlink) + } + fromstat, err := Stat(from) + if err != nil { + t.Fatalf("Stat(%q) failed: %v", from, err) + } + if !SameFile(tostat, fromstat) { + t.Errorf("Symlink(%q, %q) did not create symlink", to, from) + } + fromstat, err = Lstat(from) + if err != nil { + t.Fatalf("Lstat(%q) failed: %v", from, err) + } + if fromstat.Mode()&ModeSymlink == 0 { + t.Fatalf("Lstat(%q).Mode()&ModeSymlink = 0, want %v", from, ModeSymlink) + } + fromstat, err = Stat(from) + if err != nil { + t.Fatalf("Stat(%q) failed: %v", from, err) + } + if fromstat.Name() != from { + t.Errorf("Stat(%q).Name() = %q, want %q", from, fromstat.Name(), from) + } + if fromstat.Mode()&ModeSymlink != 0 { + t.Fatalf("Stat(%q).Mode()&ModeSymlink = %v, want 0", from, fromstat.Mode()&ModeSymlink) + } + s, err := Readlink(from) + if err != nil { + t.Fatalf("Readlink(%q) failed: %v", from, err) + } + if s != to { + t.Fatalf("Readlink(%q) = %q, want %q", from, s, to) + } + file, err = Open(from) + if err != nil { + t.Fatalf("Open(%q) failed: %v", from, err) + } + file.Close() +} + +func TestLongSymlink(t *testing.T) { + testenv.MustHaveSymlink(t) + + defer chtmpdir(t)() + s := "0123456789abcdef" + // Long, but not too long: a common limit is 255. + s = s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + from := "longsymlinktestfrom" + err := Symlink(s, from) + if err != nil { + t.Fatalf("symlink %q, %q failed: %v", s, from, err) + } + r, err := Readlink(from) + if err != nil { + t.Fatalf("readlink %q failed: %v", from, err) + } + if r != s { + t.Fatalf("after symlink %q != %q", r, s) + } +} + +func TestRename(t *testing.T) { + defer chtmpdir(t)() + from, to := "renamefrom", "renameto" + + file, err := Create(from) + if err != nil { + t.Fatalf("open %q failed: %v", from, err) + } + if err = file.Close(); err != nil { + t.Errorf("close %q failed: %v", from, err) + } + err = Rename(from, to) + if err != nil { + t.Fatalf("rename %q, %q failed: %v", to, from, err) + } + _, err = Stat(to) + if err != nil { + t.Errorf("stat %q failed: %v", to, err) + } +} + +func TestRenameOverwriteDest(t *testing.T) { + defer chtmpdir(t)() + from, to := "renamefrom", "renameto" + + toData := []byte("to") + fromData := []byte("from") + + err := WriteFile(to, toData, 0777) + if err != nil { + t.Fatalf("write file %q failed: %v", to, err) + } + + err = WriteFile(from, fromData, 0777) + if err != nil { + t.Fatalf("write file %q failed: %v", from, err) + } + err = Rename(from, to) + if err != nil { + t.Fatalf("rename %q, %q failed: %v", to, from, err) + } + + _, err = Stat(from) + if err == nil { + t.Errorf("from file %q still exists", from) + } + if err != nil && !IsNotExist(err) { + t.Fatalf("stat from: %v", err) + } + toFi, err := Stat(to) + if err != nil { + t.Fatalf("stat %q failed: %v", to, err) + } + if toFi.Size() != int64(len(fromData)) { + t.Errorf(`"to" size = %d; want %d (old "from" size)`, toFi.Size(), len(fromData)) + } +} + +func TestRenameFailed(t *testing.T) { + defer chtmpdir(t)() + from, to := "renamefrom", "renameto" + + err := Rename(from, to) + switch err := err.(type) { + case *LinkError: + if err.Op != "rename" { + t.Errorf("rename %q, %q: err.Op: want %q, got %q", from, to, "rename", err.Op) + } + if err.Old != from { + t.Errorf("rename %q, %q: err.Old: want %q, got %q", from, to, from, err.Old) + } + if err.New != to { + t.Errorf("rename %q, %q: err.New: want %q, got %q", from, to, to, err.New) + } + case nil: + t.Errorf("rename %q, %q: expected error, got nil", from, to) + default: + t.Errorf("rename %q, %q: expected %T, got %T %v", from, to, new(LinkError), err, err) + } +} + +func TestRenameNotExisting(t *testing.T) { + defer chtmpdir(t)() + from, to := "doesnt-exist", "dest" + + Mkdir(to, 0777) + + if err := Rename(from, to); !IsNotExist(err) { + t.Errorf("Rename(%q, %q) = %v; want an IsNotExist error", from, to, err) + } +} + +func TestRenameToDirFailed(t *testing.T) { + defer chtmpdir(t)() + from, to := "renamefrom", "renameto" + + Mkdir(from, 0777) + Mkdir(to, 0777) + + err := Rename(from, to) + switch err := err.(type) { + case *LinkError: + if err.Op != "rename" { + t.Errorf("rename %q, %q: err.Op: want %q, got %q", from, to, "rename", err.Op) + } + if err.Old != from { + t.Errorf("rename %q, %q: err.Old: want %q, got %q", from, to, from, err.Old) + } + if err.New != to { + t.Errorf("rename %q, %q: err.New: want %q, got %q", from, to, to, err.New) + } + case nil: + t.Errorf("rename %q, %q: expected error, got nil", from, to) + default: + t.Errorf("rename %q, %q: expected %T, got %T %v", from, to, new(LinkError), err, err) + } +} + +func TestRenameCaseDifference(pt *testing.T) { + from, to := "renameFROM", "RENAMEfrom" + tests := []struct { + name string + create func() error + }{ + {"dir", func() error { + return Mkdir(from, 0777) + }}, + {"file", func() error { + fd, err := Create(from) + if err != nil { + return err + } + return fd.Close() + }}, + } + + for _, test := range tests { + pt.Run(test.name, func(t *testing.T) { + defer chtmpdir(t)() + + if err := test.create(); err != nil { + t.Fatalf("failed to create test file: %s", err) + } + + if _, err := Stat(to); err != nil { + // Sanity check that the underlying filesystem is not case sensitive. + if IsNotExist(err) { + t.Skipf("case sensitive filesystem") + } + t.Fatalf("stat %q, got: %q", to, err) + } + + if err := Rename(from, to); err != nil { + t.Fatalf("unexpected error when renaming from %q to %q: %s", from, to, err) + } + + fd, err := Open(".") + if err != nil { + t.Fatalf("Open .: %s", err) + } + + // Stat does not return the real case of the file (it returns what the called asked for) + // So we have to use readdir to get the real name of the file. + dirNames, err := fd.Readdirnames(-1) + if err != nil { + t.Fatalf("readdirnames: %s", err) + } + + if dirNamesLen := len(dirNames); dirNamesLen != 1 { + t.Fatalf("unexpected dirNames len, got %q, want %q", dirNamesLen, 1) + } + + if dirNames[0] != to { + t.Errorf("unexpected name, got %q, want %q", dirNames[0], to) + } + }) + } +} + +func testStartProcess(dir, cmd string, args []string, expect string) func(t *testing.T) { + return func(t *testing.T) { + t.Parallel() + + r, w, err := Pipe() + if err != nil { + t.Fatalf("Pipe: %v", err) + } + defer r.Close() + attr := &ProcAttr{Dir: dir, Files: []*File{nil, w, Stderr}} + p, err := StartProcess(cmd, args, attr) + if err != nil { + t.Fatalf("StartProcess: %v", err) + } + w.Close() + + var b strings.Builder + io.Copy(&b, r) + output := b.String() + + fi1, _ := Stat(strings.TrimSpace(output)) + fi2, _ := Stat(expect) + if !SameFile(fi1, fi2) { + t.Errorf("exec %q returned %q wanted %q", + strings.Join(append([]string{cmd}, args...), " "), output, expect) + } + p.Wait() + } +} + +func TestStartProcess(t *testing.T) { + testenv.MustHaveExec(t) + t.Parallel() + + var dir, cmd string + var args []string + switch runtime.GOOS { + case "android": + t.Skip("android doesn't have /bin/pwd") + case "windows": + cmd = Getenv("COMSPEC") + dir = Getenv("SystemRoot") + args = []string{"/c", "cd"} + default: + var err error + cmd, err = exec.LookPath("pwd") + if err != nil { + t.Fatalf("Can't find pwd: %v", err) + } + dir = "/" + args = []string{} + t.Logf("Testing with %v", cmd) + } + cmddir, cmdbase := filepath.Split(cmd) + args = append([]string{cmdbase}, args...) + t.Run("absolute", testStartProcess(dir, cmd, args, dir)) + t.Run("relative", testStartProcess(cmddir, cmdbase, args, cmddir)) +} + +func checkMode(t *testing.T, path string, mode FileMode) { + dir, err := Stat(path) + if err != nil { + t.Fatalf("Stat %q (looking for mode %#o): %s", path, mode, err) + } + if dir.Mode()&ModePerm != mode { + t.Errorf("Stat %q: mode %#o want %#o", path, dir.Mode(), mode) + } +} + +func TestChmod(t *testing.T) { + // Chmod is not supported on wasip1. + if runtime.GOOS == "wasip1" { + t.Skip("Chmod is not supported on " + runtime.GOOS) + } + t.Parallel() + + f := newFile("TestChmod", t) + defer Remove(f.Name()) + defer f.Close() + // Creation mode is read write + + fm := FileMode(0456) + if runtime.GOOS == "windows" { + fm = FileMode(0444) // read-only file + } + if err := Chmod(f.Name(), fm); err != nil { + t.Fatalf("chmod %s %#o: %s", f.Name(), fm, err) + } + checkMode(t, f.Name(), fm) + + fm = FileMode(0123) + if runtime.GOOS == "windows" { + fm = FileMode(0666) // read-write file + } + if err := f.Chmod(fm); err != nil { + t.Fatalf("chmod %s %#o: %s", f.Name(), fm, err) + } + checkMode(t, f.Name(), fm) +} + +func checkSize(t *testing.T, f *File, size int64) { + t.Helper() + dir, err := f.Stat() + if err != nil { + t.Fatalf("Stat %q (looking for size %d): %s", f.Name(), size, err) + } + if dir.Size() != size { + t.Errorf("Stat %q: size %d want %d", f.Name(), dir.Size(), size) + } +} + +func TestFTruncate(t *testing.T) { + t.Parallel() + + f := newFile("TestFTruncate", t) + defer Remove(f.Name()) + defer f.Close() + + checkSize(t, f, 0) + f.Write([]byte("hello, world\n")) + checkSize(t, f, 13) + f.Truncate(10) + checkSize(t, f, 10) + f.Truncate(1024) + checkSize(t, f, 1024) + f.Truncate(0) + checkSize(t, f, 0) + _, err := f.Write([]byte("surprise!")) + if err == nil { + checkSize(t, f, 13+9) // wrote at offset past where hello, world was. + } +} + +func TestTruncate(t *testing.T) { + t.Parallel() + + f := newFile("TestTruncate", t) + defer Remove(f.Name()) + defer f.Close() + + checkSize(t, f, 0) + f.Write([]byte("hello, world\n")) + checkSize(t, f, 13) + Truncate(f.Name(), 10) + checkSize(t, f, 10) + Truncate(f.Name(), 1024) + checkSize(t, f, 1024) + Truncate(f.Name(), 0) + checkSize(t, f, 0) + _, err := f.Write([]byte("surprise!")) + if err == nil { + checkSize(t, f, 13+9) // wrote at offset past where hello, world was. + } +} + +func TestTruncateNonexistentFile(t *testing.T) { + t.Parallel() + + assertPathError := func(t testing.TB, path string, err error) { + t.Helper() + if pe, ok := err.(*PathError); !ok || !IsNotExist(err) || pe.Path != path { + t.Errorf("got error: %v\nwant an ErrNotExist PathError with path %q", err, path) + } + } + + path := filepath.Join(t.TempDir(), "nonexistent") + + err := Truncate(path, 1) + assertPathError(t, path, err) + + // Truncate shouldn't create any new file. + _, err = Stat(path) + assertPathError(t, path, err) +} + +// Use TempDir (via newFile) to make sure we're on a local file system, +// so that timings are not distorted by latency and caching. +// On NFS, timings can be off due to caching of meta-data on +// NFS servers (Issue 848). +func TestChtimes(t *testing.T) { + t.Parallel() + + f := newFile("TestChtimes", t) + defer Remove(f.Name()) + + f.Write([]byte("hello, world\n")) + f.Close() + + testChtimes(t, f.Name()) +} + +func TestChtimesWithZeroTimes(t *testing.T) { + file := newFile("chtimes-with-zero", t) + _, err := file.Write([]byte("hello, world\n")) + if err != nil { + t.Fatalf("Write: %s", err) + } + fName := file.Name() + defer Remove(file.Name()) + err = file.Close() + if err != nil { + t.Errorf("%v", err) + } + fs, err := Stat(fName) + if err != nil { + t.Fatal(err) + } + startAtime := Atime(fs) + startMtime := fs.ModTime() + switch runtime.GOOS { + case "js": + startAtime = startAtime.Truncate(time.Second) + startMtime = startMtime.Truncate(time.Second) + } + at0 := startAtime + mt0 := startMtime + t0 := startMtime.Truncate(time.Second).Add(1 * time.Hour) + + tests := []struct { + aTime time.Time + mTime time.Time + wantATime time.Time + wantMTime time.Time + }{ + { + aTime: time.Time{}, + mTime: time.Time{}, + wantATime: startAtime, + wantMTime: startMtime, + }, + { + aTime: t0.Add(200 * time.Second), + mTime: time.Time{}, + wantATime: t0.Add(200 * time.Second), + wantMTime: startMtime, + }, + { + aTime: time.Time{}, + mTime: t0.Add(100 * time.Second), + wantATime: t0.Add(200 * time.Second), + wantMTime: t0.Add(100 * time.Second), + }, + { + aTime: t0.Add(300 * time.Second), + mTime: t0.Add(100 * time.Second), + wantATime: t0.Add(300 * time.Second), + wantMTime: t0.Add(100 * time.Second), + }, + } + + for _, tt := range tests { + // Now change the times accordingly. + if err := Chtimes(fName, tt.aTime, tt.mTime); err != nil { + t.Error(err) + } + + // Finally verify the expectations. + fs, err = Stat(fName) + if err != nil { + t.Error(err) + } + at0 = Atime(fs) + mt0 = fs.ModTime() + + if got, want := at0, tt.wantATime; !got.Equal(want) { + errormsg := fmt.Sprintf("AccessTime mismatch with values ATime:%q-MTime:%q\ngot: %q\nwant: %q", tt.aTime, tt.mTime, got, want) + switch runtime.GOOS { + case "plan9": + // Mtime is the time of the last change of + // content. Similarly, atime is set whenever + // the contents are accessed; also, it is set + // whenever mtime is set. + case "windows": + t.Error(errormsg) + default: // unix's + if got, want := at0, tt.wantATime; !got.Equal(want) { + mounts, err := ReadFile("/bin/mounts") + if err != nil { + mounts, err = ReadFile("/etc/mtab") + } + if strings.Contains(string(mounts), "noatime") { + t.Log(errormsg) + t.Log("A filesystem is mounted with noatime; ignoring.") + } else { + switch runtime.GOOS { + case "netbsd", "dragonfly": + // On a 64-bit implementation, birth time is generally supported and cannot be changed. + // When supported, atime update is restricted and depends on the file system and on the + // OS configuration. + if strings.Contains(runtime.GOARCH, "64") { + t.Log(errormsg) + t.Log("Filesystem might not support atime changes; ignoring.") + } + default: + t.Error(errormsg) + } + } + } + } + } + if got, want := mt0, tt.wantMTime; !got.Equal(want) { + errormsg := fmt.Sprintf("ModTime mismatch with values ATime:%q-MTime:%q\ngot: %q\nwant: %q", tt.aTime, tt.mTime, got, want) + switch runtime.GOOS { + case "dragonfly": + t.Log(errormsg) + t.Log("Mtime is always updated; ignoring.") + default: + t.Error(errormsg) + } + } + } +} + +// Use TempDir (via newDir) to make sure we're on a local file system, +// so that timings are not distorted by latency and caching. +// On NFS, timings can be off due to caching of meta-data on +// NFS servers (Issue 848). +func TestChtimesDir(t *testing.T) { + t.Parallel() + + name := newDir("TestChtimes", t) + defer RemoveAll(name) + + testChtimes(t, name) +} + +func testChtimes(t *testing.T, name string) { + st, err := Stat(name) + if err != nil { + t.Fatalf("Stat %s: %s", name, err) + } + preStat := st + + // Move access and modification time back a second + at := Atime(preStat) + mt := preStat.ModTime() + err = Chtimes(name, at.Add(-time.Second), mt.Add(-time.Second)) + if err != nil { + t.Fatalf("Chtimes %s: %s", name, err) + } + + st, err = Stat(name) + if err != nil { + t.Fatalf("second Stat %s: %s", name, err) + } + postStat := st + + pat := Atime(postStat) + pmt := postStat.ModTime() + if !pat.Before(at) { + switch runtime.GOOS { + case "plan9": + // Mtime is the time of the last change of + // content. Similarly, atime is set whenever + // the contents are accessed; also, it is set + // whenever mtime is set. + case "netbsd": + mounts, _ := ReadFile("/proc/mounts") + if strings.Contains(string(mounts), "noatime") { + t.Logf("AccessTime didn't go backwards, but see a filesystem mounted noatime; ignoring. Issue 19293.") + } else { + t.Logf("AccessTime didn't go backwards; was=%v, after=%v (Ignoring on NetBSD, assuming noatime, Issue 19293)", at, pat) + } + default: + t.Errorf("AccessTime didn't go backwards; was=%v, after=%v", at, pat) + } + } + + if !pmt.Before(mt) { + t.Errorf("ModTime didn't go backwards; was=%v, after=%v", mt, pmt) + } +} + +func TestChtimesToUnixZero(t *testing.T) { + file := newFile("chtimes-to-unix-zero", t) + fn := file.Name() + defer Remove(fn) + if _, err := file.Write([]byte("hi")); err != nil { + t.Fatal(err) + } + if err := file.Close(); err != nil { + t.Fatal(err) + } + + unixZero := time.Unix(0, 0) + if err := Chtimes(fn, unixZero, unixZero); err != nil { + t.Fatalf("Chtimes failed: %v", err) + } + + st, err := Stat(fn) + if err != nil { + t.Fatal(err) + } + + if mt := st.ModTime(); mt != unixZero { + t.Errorf("mtime is %v, want %v", mt, unixZero) + } +} + +func TestFileChdir(t *testing.T) { + wd, err := Getwd() + if err != nil { + t.Fatalf("Getwd: %s", err) + } + defer Chdir(wd) + + fd, err := Open(".") + if err != nil { + t.Fatalf("Open .: %s", err) + } + defer fd.Close() + + if err := Chdir("/"); err != nil { + t.Fatalf("Chdir /: %s", err) + } + + if err := fd.Chdir(); err != nil { + t.Fatalf("fd.Chdir: %s", err) + } + + wdNew, err := Getwd() + if err != nil { + t.Fatalf("Getwd: %s", err) + } + if !equal(wdNew, wd) { + t.Fatalf("fd.Chdir failed, got %s, want %s", wdNew, wd) + } +} + +func TestChdirAndGetwd(t *testing.T) { + fd, err := Open(".") + if err != nil { + t.Fatalf("Open .: %s", err) + } + // These are chosen carefully not to be symlinks on a Mac + // (unlike, say, /var, /etc), except /tmp, which we handle below. + dirs := []string{"/", "/usr/bin", "/tmp"} + // /usr/bin does not usually exist on Plan 9 or Android. + switch runtime.GOOS { + case "android": + dirs = []string{"/system/bin"} + case "plan9": + dirs = []string{"/", "/usr"} + case "ios", "windows", "wasip1": + dirs = nil + for _, dir := range []string{t.TempDir(), t.TempDir()} { + // Expand symlinks so path equality tests work. + dir, err = filepath.EvalSymlinks(dir) + if err != nil { + t.Fatalf("EvalSymlinks: %v", err) + } + dirs = append(dirs, dir) + } + } + oldwd := Getenv("PWD") + for mode := 0; mode < 2; mode++ { + for _, d := range dirs { + if mode == 0 { + err = Chdir(d) + } else { + fd1, err1 := Open(d) + if err1 != nil { + t.Errorf("Open %s: %s", d, err1) + continue + } + err = fd1.Chdir() + fd1.Close() + } + if d == "/tmp" { + Setenv("PWD", "/tmp") + } + pwd, err1 := Getwd() + Setenv("PWD", oldwd) + err2 := fd.Chdir() + if err2 != nil { + // We changed the current directory and cannot go back. + // Don't let the tests continue; they'll scribble + // all over some other directory. + fmt.Fprintf(Stderr, "fchdir back to dot failed: %s\n", err2) + Exit(1) + } + if err != nil { + fd.Close() + t.Fatalf("Chdir %s: %s", d, err) + } + if err1 != nil { + fd.Close() + t.Fatalf("Getwd in %s: %s", d, err1) + } + if !equal(pwd, d) { + fd.Close() + t.Fatalf("Getwd returned %q want %q", pwd, d) + } + } + } + fd.Close() +} + +// Test that Chdir+Getwd is program-wide. +func TestProgWideChdir(t *testing.T) { + const N = 10 + var wg sync.WaitGroup + hold := make(chan struct{}) + done := make(chan struct{}) + + d := t.TempDir() + oldwd, err := Getwd() + if err != nil { + t.Fatalf("Getwd: %v", err) + } + defer func() { + if err := Chdir(oldwd); err != nil { + // It's not safe to continue with tests if we can't get back to + // the original working directory. + panic(err) + } + }() + + // Note the deferred Wait must be called after the deferred close(done), + // to ensure the N goroutines have been released even if the main goroutine + // calls Fatalf. It must be called before the Chdir back to the original + // directory, and before the deferred deletion implied by TempDir, + // so as not to interfere while the N goroutines are still running. + defer wg.Wait() + defer close(done) + + for i := 0; i < N; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + // Lock half the goroutines in their own operating system + // thread to exercise more scheduler possibilities. + if i%2 == 1 { + // On Plan 9, after calling LockOSThread, the goroutines + // run on different processes which don't share the working + // directory. This used to be an issue because Go expects + // the working directory to be program-wide. + // See issue 9428. + runtime.LockOSThread() + } + select { + case <-done: + return + case <-hold: + } + // Getwd might be wrong + f0, err := Stat(".") + if err != nil { + t.Error(err) + return + } + pwd, err := Getwd() + if err != nil { + t.Errorf("Getwd: %v", err) + return + } + if pwd != d { + t.Errorf("Getwd() = %q, want %q", pwd, d) + return + } + f1, err := Stat(pwd) + if err != nil { + t.Error(err) + return + } + if !SameFile(f0, f1) { + t.Errorf(`Samefile(Stat("."), Getwd()) reports false (%s != %s)`, f0.Name(), f1.Name()) + return + } + }(i) + } + if err = Chdir(d); err != nil { + t.Fatalf("Chdir: %v", err) + } + // OS X sets TMPDIR to a symbolic link. + // So we resolve our working directory again before the test. + d, err = Getwd() + if err != nil { + t.Fatalf("Getwd: %v", err) + } + close(hold) + wg.Wait() +} + +func TestSeek(t *testing.T) { + t.Parallel() + + f := newFile("TestSeek", t) + defer Remove(f.Name()) + defer f.Close() + + const data = "hello, world\n" + io.WriteString(f, data) + + type test struct { + in int64 + whence int + out int64 + } + var tests = []test{ + {0, io.SeekCurrent, int64(len(data))}, + {0, io.SeekStart, 0}, + {5, io.SeekStart, 5}, + {0, io.SeekEnd, int64(len(data))}, + {0, io.SeekStart, 0}, + {-1, io.SeekEnd, int64(len(data)) - 1}, + {1 << 33, io.SeekStart, 1 << 33}, + {1 << 33, io.SeekEnd, 1<<33 + int64(len(data))}, + + // Issue 21681, Windows 4G-1, etc: + {1<<32 - 1, io.SeekStart, 1<<32 - 1}, + {0, io.SeekCurrent, 1<<32 - 1}, + {2<<32 - 1, io.SeekStart, 2<<32 - 1}, + {0, io.SeekCurrent, 2<<32 - 1}, + } + for i, tt := range tests { + off, err := f.Seek(tt.in, tt.whence) + if off != tt.out || err != nil { + if e, ok := err.(*PathError); ok && e.Err == syscall.EINVAL && tt.out > 1<<32 && runtime.GOOS == "linux" { + mounts, _ := ReadFile("/proc/mounts") + if strings.Contains(string(mounts), "reiserfs") { + // Reiserfs rejects the big seeks. + t.Skipf("skipping test known to fail on reiserfs; https://golang.org/issue/91") + } + } + t.Errorf("#%d: Seek(%v, %v) = %v, %v want %v, nil", i, tt.in, tt.whence, off, err, tt.out) + } + } +} + +func TestSeekError(t *testing.T) { + switch runtime.GOOS { + case "js", "plan9", "wasip1": + t.Skipf("skipping test on %v", runtime.GOOS) + } + t.Parallel() + + r, w, err := Pipe() + if err != nil { + t.Fatal(err) + } + _, err = r.Seek(0, 0) + if err == nil { + t.Fatal("Seek on pipe should fail") + } + if perr, ok := err.(*PathError); !ok || perr.Err != syscall.ESPIPE { + t.Errorf("Seek returned error %v, want &PathError{Err: syscall.ESPIPE}", err) + } + _, err = w.Seek(0, 0) + if err == nil { + t.Fatal("Seek on pipe should fail") + } + if perr, ok := err.(*PathError); !ok || perr.Err != syscall.ESPIPE { + t.Errorf("Seek returned error %v, want &PathError{Err: syscall.ESPIPE}", err) + } +} + +type openErrorTest struct { + path string + mode int + error error +} + +var openErrorTests = []openErrorTest{ + { + sfdir + "/no-such-file", + O_RDONLY, + syscall.ENOENT, + }, + { + sfdir, + O_WRONLY, + syscall.EISDIR, + }, + { + sfdir + "/" + sfname + "/no-such-file", + O_WRONLY, + syscall.ENOTDIR, + }, +} + +func TestOpenError(t *testing.T) { + t.Parallel() + + for _, tt := range openErrorTests { + f, err := OpenFile(tt.path, tt.mode, 0) + if err == nil { + t.Errorf("Open(%q, %d) succeeded", tt.path, tt.mode) + f.Close() + continue + } + perr, ok := err.(*PathError) + if !ok { + t.Errorf("Open(%q, %d) returns error of %T type; want *PathError", tt.path, tt.mode, err) + } + if perr.Err != tt.error { + if runtime.GOOS == "plan9" { + syscallErrStr := perr.Err.Error() + expectedErrStr := strings.Replace(tt.error.Error(), "file ", "", 1) + if !strings.HasSuffix(syscallErrStr, expectedErrStr) { + // Some Plan 9 file servers incorrectly return + // EACCES rather than EISDIR when a directory is + // opened for write. + if tt.error == syscall.EISDIR && strings.HasSuffix(syscallErrStr, syscall.EACCES.Error()) { + continue + } + t.Errorf("Open(%q, %d) = _, %q; want suffix %q", tt.path, tt.mode, syscallErrStr, expectedErrStr) + } + continue + } + if runtime.GOOS == "dragonfly" { + // DragonFly incorrectly returns EACCES rather + // EISDIR when a directory is opened for write. + if tt.error == syscall.EISDIR && perr.Err == syscall.EACCES { + continue + } + } + t.Errorf("Open(%q, %d) = _, %q; want %q", tt.path, tt.mode, perr.Err.Error(), tt.error.Error()) + } + } +} + +func TestOpenNoName(t *testing.T) { + f, err := Open("") + if err == nil { + f.Close() + t.Fatal(`Open("") succeeded`) + } +} + +func runBinHostname(t *testing.T) string { + // Run /bin/hostname and collect output. + r, w, err := Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + + path, err := exec.LookPath("hostname") + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + t.Skip("skipping test; test requires hostname but it does not exist") + } + t.Fatal(err) + } + + argv := []string{"hostname"} + if runtime.GOOS == "aix" { + argv = []string{"hostname", "-s"} + } + p, err := StartProcess(path, argv, &ProcAttr{Files: []*File{nil, w, Stderr}}) + if err != nil { + t.Fatal(err) + } + w.Close() + + var b strings.Builder + io.Copy(&b, r) + _, err = p.Wait() + if err != nil { + t.Fatalf("run hostname Wait: %v", err) + } + err = p.Kill() + if err == nil { + t.Errorf("expected an error from Kill running 'hostname'") + } + output := b.String() + if n := len(output); n > 0 && output[n-1] == '\n' { + output = output[0 : n-1] + } + if output == "" { + t.Fatalf("/bin/hostname produced no output") + } + + return output +} + +func testWindowsHostname(t *testing.T, hostname string) { + cmd := testenv.Command(t, "hostname") + out, err := cmd.Output() + if err != nil { + t.Fatalf("Failed to execute hostname command: %v %s", err, out) + } + want := strings.Trim(string(out), "\r\n") + if hostname != want { + t.Fatalf("Hostname() = %q != system hostname of %q", hostname, want) + } +} + +func TestHostname(t *testing.T) { + t.Parallel() + + hostname, err := Hostname() + if err != nil { + t.Fatal(err) + } + if hostname == "" { + t.Fatal("Hostname returned empty string and no error") + } + if strings.Contains(hostname, "\x00") { + t.Fatalf("unexpected zero byte in hostname: %q", hostname) + } + + // There is no other way to fetch hostname on windows, but via winapi. + // On Plan 9 it can be taken from #c/sysname as Hostname() does. + switch runtime.GOOS { + case "android", "plan9": + // No /bin/hostname to verify against. + return + case "windows": + testWindowsHostname(t, hostname) + return + } + + testenv.MustHaveExec(t) + + // Check internal Hostname() against the output of /bin/hostname. + // Allow that the internal Hostname returns a Fully Qualified Domain Name + // and the /bin/hostname only returns the first component + want := runBinHostname(t) + if hostname != want { + host, _, ok := strings.Cut(hostname, ".") + if !ok || host != want { + t.Errorf("Hostname() = %q, want %q", hostname, want) + } + } +} + +func TestReadAt(t *testing.T) { + t.Parallel() + + f := newFile("TestReadAt", t) + defer Remove(f.Name()) + defer f.Close() + + const data = "hello, world\n" + io.WriteString(f, data) + + b := make([]byte, 5) + n, err := f.ReadAt(b, 7) + if err != nil || n != len(b) { + t.Fatalf("ReadAt 7: %d, %v", n, err) + } + if string(b) != "world" { + t.Fatalf("ReadAt 7: have %q want %q", string(b), "world") + } +} + +// Verify that ReadAt doesn't affect seek offset. +// In the Plan 9 kernel, there used to be a bug in the implementation of +// the pread syscall, where the channel offset was erroneously updated after +// calling pread on a file. +func TestReadAtOffset(t *testing.T) { + t.Parallel() + + f := newFile("TestReadAtOffset", t) + defer Remove(f.Name()) + defer f.Close() + + const data = "hello, world\n" + io.WriteString(f, data) + + f.Seek(0, 0) + b := make([]byte, 5) + + n, err := f.ReadAt(b, 7) + if err != nil || n != len(b) { + t.Fatalf("ReadAt 7: %d, %v", n, err) + } + if string(b) != "world" { + t.Fatalf("ReadAt 7: have %q want %q", string(b), "world") + } + + n, err = f.Read(b) + if err != nil || n != len(b) { + t.Fatalf("Read: %d, %v", n, err) + } + if string(b) != "hello" { + t.Fatalf("Read: have %q want %q", string(b), "hello") + } +} + +// Verify that ReadAt doesn't allow negative offset. +func TestReadAtNegativeOffset(t *testing.T) { + t.Parallel() + + f := newFile("TestReadAtNegativeOffset", t) + defer Remove(f.Name()) + defer f.Close() + + const data = "hello, world\n" + io.WriteString(f, data) + + f.Seek(0, 0) + b := make([]byte, 5) + + n, err := f.ReadAt(b, -10) + + const wantsub = "negative offset" + if !strings.Contains(fmt.Sprint(err), wantsub) || n != 0 { + t.Errorf("ReadAt(-10) = %v, %v; want 0, ...%q...", n, err, wantsub) + } +} + +func TestWriteAt(t *testing.T) { + t.Parallel() + + 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 := 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) { + t.Parallel() + + 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) { + defer chtmpdir(t)() + f, err := OpenFile("write_at_in_append_mode.txt", O_APPEND|O_CREATE, 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) + } +} + +func writeFile(t *testing.T, fname string, flag int, text string) string { + f, err := OpenFile(fname, flag, 0666) + if err != nil { + t.Fatalf("Open: %v", err) + } + n, err := io.WriteString(f, text) + if err != nil { + t.Fatalf("WriteString: %d, %v", n, err) + } + f.Close() + data, err := ReadFile(fname) + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + return string(data) +} + +func TestAppend(t *testing.T) { + defer chtmpdir(t)() + const f = "append.txt" + s := writeFile(t, f, O_CREATE|O_TRUNC|O_RDWR, "new") + if s != "new" { + t.Fatalf("writeFile: have %q want %q", s, "new") + } + s = writeFile(t, f, O_APPEND|O_RDWR, "|append") + if s != "new|append" { + t.Fatalf("writeFile: have %q want %q", s, "new|append") + } + s = writeFile(t, f, O_CREATE|O_APPEND|O_RDWR, "|append") + if s != "new|append|append" { + t.Fatalf("writeFile: have %q want %q", s, "new|append|append") + } + err := Remove(f) + if err != nil { + t.Fatalf("Remove: %v", err) + } + s = writeFile(t, f, O_CREATE|O_APPEND|O_RDWR, "new&append") + if s != "new&append" { + t.Fatalf("writeFile: after append have %q want %q", s, "new&append") + } + s = writeFile(t, f, O_CREATE|O_RDWR, "old") + if s != "old&append" { + t.Fatalf("writeFile: after create have %q want %q", s, "old&append") + } + s = writeFile(t, f, O_CREATE|O_TRUNC|O_RDWR, "new") + if s != "new" { + t.Fatalf("writeFile: after truncate have %q want %q", s, "new") + } +} + +func TestStatDirWithTrailingSlash(t *testing.T) { + t.Parallel() + + // Create new temporary directory and arrange to clean it up. + path := t.TempDir() + + // Stat of path should succeed. + if _, err := Stat(path); err != nil { + t.Fatalf("stat %s failed: %s", path, err) + } + + // Stat of path+"/" should succeed too. + path += "/" + if _, err := Stat(path); err != nil { + t.Fatalf("stat %s failed: %s", path, err) + } +} + +func TestNilProcessStateString(t *testing.T) { + var ps *ProcessState + s := ps.String() + if s != "" { + t.Errorf("(*ProcessState)(nil).String() = %q, want %q", s, "") + } +} + +func TestSameFile(t *testing.T) { + defer chtmpdir(t)() + fa, err := Create("a") + if err != nil { + t.Fatalf("Create(a): %v", err) + } + fa.Close() + fb, err := Create("b") + if err != nil { + t.Fatalf("Create(b): %v", err) + } + fb.Close() + + ia1, err := Stat("a") + if err != nil { + t.Fatalf("Stat(a): %v", err) + } + ia2, err := Stat("a") + if err != nil { + t.Fatalf("Stat(a): %v", err) + } + if !SameFile(ia1, ia2) { + t.Errorf("files should be same") + } + + ib, err := Stat("b") + if err != nil { + t.Fatalf("Stat(b): %v", err) + } + if SameFile(ia1, ib) { + t.Errorf("files should be different") + } +} + +func testDevNullFileInfo(t *testing.T, statname, devNullName string, fi FileInfo) { + pre := fmt.Sprintf("%s(%q): ", statname, devNullName) + if fi.Size() != 0 { + t.Errorf(pre+"wrong file size have %d want 0", fi.Size()) + } + if fi.Mode()&ModeDevice == 0 { + t.Errorf(pre+"wrong file mode %q: ModeDevice is not set", fi.Mode()) + } + if fi.Mode()&ModeCharDevice == 0 { + t.Errorf(pre+"wrong file mode %q: ModeCharDevice is not set", fi.Mode()) + } + if fi.Mode().IsRegular() { + t.Errorf(pre+"wrong file mode %q: IsRegular returns true", fi.Mode()) + } +} + +func testDevNullFile(t *testing.T, devNullName string) { + f, err := Open(devNullName) + if err != nil { + t.Fatalf("Open(%s): %v", devNullName, err) + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + t.Fatalf("Stat(%s): %v", devNullName, err) + } + testDevNullFileInfo(t, "f.Stat", devNullName, fi) + + fi, err = Stat(devNullName) + if err != nil { + t.Fatalf("Stat(%s): %v", devNullName, err) + } + testDevNullFileInfo(t, "Stat", devNullName, fi) +} + +func TestDevNullFile(t *testing.T) { + t.Parallel() + + testDevNullFile(t, DevNull) + if runtime.GOOS == "windows" { + testDevNullFile(t, "./nul") + testDevNullFile(t, "//./nul") + } +} + +var testLargeWrite = flag.Bool("large_write", false, "run TestLargeWriteToConsole test that floods console with output") + +func TestLargeWriteToConsole(t *testing.T) { + if !*testLargeWrite { + t.Skip("skipping console-flooding test; enable with -large_write") + } + b := make([]byte, 32000) + for i := range b { + b[i] = '.' + } + b[len(b)-1] = '\n' + n, err := Stdout.Write(b) + if err != nil { + t.Fatalf("Write to os.Stdout failed: %v", err) + } + if n != len(b) { + t.Errorf("Write to os.Stdout should return %d; got %d", len(b), n) + } + n, err = Stderr.Write(b) + if err != nil { + t.Fatalf("Write to os.Stderr failed: %v", err) + } + if n != len(b) { + t.Errorf("Write to os.Stderr should return %d; got %d", len(b), n) + } +} + +func TestStatDirModeExec(t *testing.T) { + if runtime.GOOS == "wasip1" { + t.Skip("Chmod is not supported on " + runtime.GOOS) + } + t.Parallel() + + const mode = 0111 + + path := t.TempDir() + if err := Chmod(path, 0777); err != nil { + t.Fatalf("Chmod %q 0777: %v", path, err) + } + + dir, err := Stat(path) + if err != nil { + t.Fatalf("Stat %q (looking for mode %#o): %s", path, mode, err) + } + if dir.Mode()&mode != mode { + t.Errorf("Stat %q: mode %#o want %#o", path, dir.Mode()&mode, mode) + } +} + +func TestStatStdin(t *testing.T) { + switch runtime.GOOS { + case "android", "plan9": + t.Skipf("%s doesn't have /bin/sh", runtime.GOOS) + } + + if Getenv("GO_WANT_HELPER_PROCESS") == "1" { + st, err := Stdin.Stat() + if err != nil { + t.Fatalf("Stat failed: %v", err) + } + fmt.Println(st.Mode() & ModeNamedPipe) + Exit(0) + } + + testenv.MustHaveExec(t) + t.Parallel() + + fi, err := Stdin.Stat() + if err != nil { + t.Fatal(err) + } + switch mode := fi.Mode(); { + case mode&ModeCharDevice != 0 && mode&ModeDevice != 0: + case mode&ModeNamedPipe != 0: + default: + t.Fatalf("unexpected Stdin mode (%v), want ModeCharDevice or ModeNamedPipe", mode) + } + + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = testenv.Command(t, "cmd", "/c", "echo output | "+Args[0]+" -test.run=TestStatStdin") + } else { + cmd = testenv.Command(t, "/bin/sh", "-c", "echo output | "+Args[0]+" -test.run=TestStatStdin") + } + cmd.Env = append(Environ(), "GO_WANT_HELPER_PROCESS=1") + + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Failed to spawn child process: %v %q", err, string(output)) + } + + // result will be like "prw-rw-rw" + if len(output) < 1 || output[0] != 'p' { + t.Fatalf("Child process reports stdin is not pipe '%v'", string(output)) + } +} + +func TestStatRelativeSymlink(t *testing.T) { + testenv.MustHaveSymlink(t) + t.Parallel() + + tmpdir := t.TempDir() + target := filepath.Join(tmpdir, "target") + f, err := Create(target) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + st, err := f.Stat() + if err != nil { + t.Fatal(err) + } + + link := filepath.Join(tmpdir, "link") + err = Symlink(filepath.Base(target), link) + if err != nil { + t.Fatal(err) + } + + st1, err := Stat(link) + if err != nil { + t.Fatal(err) + } + + if !SameFile(st, st1) { + t.Error("Stat doesn't follow relative symlink") + } + + if runtime.GOOS == "windows" { + Remove(link) + err = Symlink(target[len(filepath.VolumeName(target)):], link) + if err != nil { + t.Fatal(err) + } + + st1, err := Stat(link) + if err != nil { + t.Fatal(err) + } + + if !SameFile(st, st1) { + t.Error("Stat doesn't follow relative symlink") + } + } +} + +func TestReadAtEOF(t *testing.T) { + t.Parallel() + + f := newFile("TestReadAtEOF", t) + defer Remove(f.Name()) + defer f.Close() + + _, err := f.ReadAt(make([]byte, 10), 0) + switch err { + case io.EOF: + // all good + case nil: + t.Fatalf("ReadAt succeeded") + default: + t.Fatalf("ReadAt failed: %s", err) + } +} + +func TestLongPath(t *testing.T) { + t.Parallel() + + tmpdir := newDir("TestLongPath", t) + defer func(d string) { + if err := RemoveAll(d); err != nil { + t.Fatalf("RemoveAll failed: %v", err) + } + }(tmpdir) + + // Test the boundary of 247 and fewer bytes (normal) and 248 and more bytes (adjusted). + sizes := []int{247, 248, 249, 400} + for len(tmpdir) < 400 { + tmpdir += "/dir3456789" + } + for _, sz := range sizes { + t.Run(fmt.Sprintf("length=%d", sz), func(t *testing.T) { + sizedTempDir := tmpdir[:sz-1] + "x" // Ensure it does not end with a slash. + + // The various sized runs are for this call to trigger the boundary + // condition. + if err := MkdirAll(sizedTempDir, 0755); err != nil { + t.Fatalf("MkdirAll failed: %v", err) + } + data := []byte("hello world\n") + if err := WriteFile(sizedTempDir+"/foo.txt", data, 0644); err != nil { + t.Fatalf("os.WriteFile() failed: %v", err) + } + if err := Rename(sizedTempDir+"/foo.txt", sizedTempDir+"/bar.txt"); err != nil { + t.Fatalf("Rename failed: %v", err) + } + mtime := time.Now().Truncate(time.Minute) + if err := Chtimes(sizedTempDir+"/bar.txt", mtime, mtime); err != nil { + t.Fatalf("Chtimes failed: %v", err) + } + names := []string{"bar.txt"} + if testenv.HasSymlink() { + if err := Symlink(sizedTempDir+"/bar.txt", sizedTempDir+"/symlink.txt"); err != nil { + t.Fatalf("Symlink failed: %v", err) + } + names = append(names, "symlink.txt") + } + if testenv.HasLink() { + if err := Link(sizedTempDir+"/bar.txt", sizedTempDir+"/link.txt"); err != nil { + t.Fatalf("Link failed: %v", err) + } + names = append(names, "link.txt") + } + for _, wantSize := range []int64{int64(len(data)), 0} { + for _, name := range names { + path := sizedTempDir + "/" + name + dir, err := Stat(path) + if err != nil { + t.Fatalf("Stat(%q) failed: %v", path, err) + } + filesize := size(path, t) + if dir.Size() != filesize || filesize != wantSize { + t.Errorf("Size(%q) is %d, len(ReadFile()) is %d, want %d", path, dir.Size(), filesize, wantSize) + } + if runtime.GOOS != "wasip1" { // Chmod is not supported on wasip1 + err = Chmod(path, dir.Mode()) + if err != nil { + t.Fatalf("Chmod(%q) failed: %v", path, err) + } + } + } + if err := Truncate(sizedTempDir+"/bar.txt", 0); err != nil { + t.Fatalf("Truncate failed: %v", err) + } + } + }) + } +} + +func testKillProcess(t *testing.T, processKiller func(p *Process)) { + testenv.MustHaveExec(t) + t.Parallel() + + // Re-exec the test binary to start a process that hangs until stdin is closed. + cmd := testenv.Command(t, Args[0]) + cmd.Env = append(cmd.Environ(), "GO_OS_TEST_DRAIN_STDIN=1") + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + err = cmd.Start() + if err != nil { + t.Fatalf("Failed to start test process: %v", err) + } + + defer func() { + if err := cmd.Wait(); err == nil { + t.Errorf("Test process succeeded, but expected to fail") + } + stdin.Close() // Keep stdin alive until the process has finished dying. + }() + + // Wait for the process to be started. + // (It will close its stdout when it reaches TestMain.) + io.Copy(io.Discard, stdout) + + processKiller(cmd.Process) +} + +func TestKillStartProcess(t *testing.T) { + testKillProcess(t, func(p *Process) { + err := p.Kill() + if err != nil { + t.Fatalf("Failed to kill test process: %v", err) + } + }) +} + +func TestGetppid(t *testing.T) { + if runtime.GOOS == "plan9" { + // TODO: golang.org/issue/8206 + t.Skipf("skipping test on plan9; see issue 8206") + } + + if Getenv("GO_WANT_HELPER_PROCESS") == "1" { + fmt.Print(Getppid()) + Exit(0) + } + + testenv.MustHaveExec(t) + t.Parallel() + + cmd := testenv.Command(t, Args[0], "-test.run=TestGetppid") + cmd.Env = append(Environ(), "GO_WANT_HELPER_PROCESS=1") + + // verify that Getppid() from the forked process reports our process id + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Failed to spawn child process: %v %q", err, string(output)) + } + + childPpid := string(output) + ourPid := fmt.Sprintf("%d", Getpid()) + if childPpid != ourPid { + t.Fatalf("Child process reports parent process id '%v', expected '%v'", childPpid, ourPid) + } +} + +func TestKillFindProcess(t *testing.T) { + testKillProcess(t, func(p *Process) { + p2, err := FindProcess(p.Pid) + if err != nil { + t.Fatalf("Failed to find test process: %v", err) + } + err = p2.Kill() + if err != nil { + t.Fatalf("Failed to kill test process: %v", err) + } + }) +} + +var nilFileMethodTests = []struct { + name string + f func(*File) error +}{ + {"Chdir", func(f *File) error { return f.Chdir() }}, + {"Close", func(f *File) error { return f.Close() }}, + {"Chmod", func(f *File) error { return f.Chmod(0) }}, + {"Chown", func(f *File) error { return f.Chown(0, 0) }}, + {"Read", func(f *File) error { _, err := f.Read(make([]byte, 0)); return err }}, + {"ReadAt", func(f *File) error { _, err := f.ReadAt(make([]byte, 0), 0); return err }}, + {"Readdir", func(f *File) error { _, err := f.Readdir(1); return err }}, + {"Readdirnames", func(f *File) error { _, err := f.Readdirnames(1); return err }}, + {"Seek", func(f *File) error { _, err := f.Seek(0, io.SeekStart); return err }}, + {"Stat", func(f *File) error { _, err := f.Stat(); return err }}, + {"Sync", func(f *File) error { return f.Sync() }}, + {"Truncate", func(f *File) error { return f.Truncate(0) }}, + {"Write", func(f *File) error { _, err := f.Write(make([]byte, 0)); return err }}, + {"WriteAt", func(f *File) error { _, err := f.WriteAt(make([]byte, 0), 0); return err }}, + {"WriteString", func(f *File) error { _, err := f.WriteString(""); return err }}, +} + +// Test that all File methods give ErrInvalid if the receiver is nil. +func TestNilFileMethods(t *testing.T) { + t.Parallel() + + for _, tt := range nilFileMethodTests { + var file *File + got := tt.f(file) + if got != ErrInvalid { + t.Errorf("%v should fail when f is nil; got %v", tt.name, got) + } + } +} + +func mkdirTree(t *testing.T, root string, level, max int) { + if level >= max { + return + } + level++ + for i := 'a'; i < 'c'; i++ { + dir := filepath.Join(root, string(i)) + if err := Mkdir(dir, 0700); err != nil { + t.Fatal(err) + } + mkdirTree(t, dir, level, max) + } +} + +// Test that simultaneous RemoveAll do not report an error. +// As long as it gets removed, we should be happy. +func TestRemoveAllRace(t *testing.T) { + if runtime.GOOS == "windows" { + // Windows has very strict rules about things like + // removing directories while someone else has + // them open. The racing doesn't work out nicely + // like it does on Unix. + t.Skip("skipping on windows") + } + if runtime.GOOS == "dragonfly" { + testenv.SkipFlaky(t, 52301) + } + + n := runtime.GOMAXPROCS(16) + defer runtime.GOMAXPROCS(n) + root, err := MkdirTemp("", "issue") + if err != nil { + t.Fatal(err) + } + mkdirTree(t, root, 1, 6) + hold := make(chan struct{}) + var wg sync.WaitGroup + for i := 0; i < 4; i++ { + wg.Add(1) + go func() { + defer wg.Done() + <-hold + err := RemoveAll(root) + if err != nil { + t.Errorf("unexpected error: %T, %q", err, err) + } + }() + } + close(hold) // let workers race to remove root + wg.Wait() +} + +// Test that reading from a pipe doesn't use up a thread. +func TestPipeThreads(t *testing.T) { + switch runtime.GOOS { + case "illumos", "solaris": + t.Skip("skipping on Solaris and illumos; issue 19111") + case "windows": + t.Skip("skipping on Windows; issue 19098") + case "plan9": + t.Skip("skipping on Plan 9; does not support runtime poller") + case "js": + t.Skip("skipping on js; no support for os.Pipe") + case "wasip1": + t.Skip("skipping on wasip1; no support for os.Pipe") + } + + threads := 100 + + // OpenBSD has a low default for max number of files. + if runtime.GOOS == "openbsd" { + threads = 50 + } + + r := make([]*File, threads) + w := make([]*File, threads) + for i := 0; i < threads; i++ { + rp, wp, err := Pipe() + if err != nil { + for j := 0; j < i; j++ { + r[j].Close() + w[j].Close() + } + t.Fatal(err) + } + r[i] = rp + w[i] = wp + } + + defer debug.SetMaxThreads(debug.SetMaxThreads(threads / 2)) + + creading := make(chan bool, threads) + cdone := make(chan bool, threads) + for i := 0; i < threads; i++ { + go func(i int) { + var b [1]byte + creading <- true + if _, err := r[i].Read(b[:]); err != nil { + t.Error(err) + } + if err := r[i].Close(); err != nil { + t.Error(err) + } + cdone <- true + }(i) + } + + for i := 0; i < threads; i++ { + <-creading + } + + // If we are still alive, it means that the 100 goroutines did + // not require 100 threads. + + for i := 0; i < threads; i++ { + if _, err := w[i].Write([]byte{0}); err != nil { + t.Error(err) + } + if err := w[i].Close(); err != nil { + t.Error(err) + } + <-cdone + } +} + +func testDoubleCloseError(path string) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + + file, err := Open(path) + if err != nil { + t.Fatal(err) + } + if err := file.Close(); err != nil { + t.Fatalf("unexpected error from Close: %v", err) + } + if err := file.Close(); err == nil { + t.Error("second Close did not fail") + } else if pe, ok := err.(*PathError); !ok { + t.Errorf("second Close: got %T, want %T", err, pe) + } else if pe.Err != ErrClosed { + t.Errorf("second Close: got %q, want %q", pe.Err, ErrClosed) + } else { + t.Logf("second close returned expected error %q", err) + } + } +} + +func TestDoubleCloseError(t *testing.T) { + t.Parallel() + t.Run("file", testDoubleCloseError(filepath.Join(sfdir, sfname))) + t.Run("dir", testDoubleCloseError(sfdir)) +} + +func TestUserHomeDir(t *testing.T) { + t.Parallel() + + dir, err := UserHomeDir() + if dir == "" && err == nil { + t.Fatal("UserHomeDir returned an empty string but no error") + } + if err != nil { + // UserHomeDir may return a non-nil error if the environment variable + // for the home directory is empty or unset in the environment. + t.Skipf("skipping: %v", err) + } + + fi, err := Stat(dir) + if err != nil { + if IsNotExist(err) { + // The user's home directory has a well-defined location, but does not + // exist. (Maybe nothing has written to it yet? That could happen, for + // example, on minimal VM images used for CI testing.) + t.Log(err) + return + } + t.Fatal(err) + } + if !fi.IsDir() { + t.Fatalf("dir %s is not directory; type = %v", dir, fi.Mode()) + } +} + +func TestDirSeek(t *testing.T) { + t.Parallel() + + wd, err := Getwd() + if err != nil { + t.Fatal(err) + } + f, err := Open(wd) + if err != nil { + t.Fatal(err) + } + dirnames1, err := f.Readdirnames(0) + if err != nil { + t.Fatal(err) + } + + ret, err := f.Seek(0, 0) + if err != nil { + t.Fatal(err) + } + if ret != 0 { + t.Fatalf("seek result not zero: %d", ret) + } + + dirnames2, err := f.Readdirnames(0) + if err != nil { + t.Fatal(err) + } + + if len(dirnames1) != len(dirnames2) { + t.Fatalf("listings have different lengths: %d and %d\n", len(dirnames1), len(dirnames2)) + } + for i, n1 := range dirnames1 { + n2 := dirnames2[i] + if n1 != n2 { + t.Fatalf("different name i=%d n1=%s n2=%s\n", i, n1, n2) + } + } +} + +func TestReaddirSmallSeek(t *testing.T) { + // See issue 37161. Read only one entry from a directory, + // seek to the beginning, and read again. We should not see + // duplicate entries. + t.Parallel() + + wd, err := Getwd() + if err != nil { + t.Fatal(err) + } + df, err := Open(filepath.Join(wd, "testdata", "issue37161")) + if err != nil { + t.Fatal(err) + } + names1, err := df.Readdirnames(1) + if err != nil { + t.Fatal(err) + } + if _, err = df.Seek(0, 0); err != nil { + t.Fatal(err) + } + names2, err := df.Readdirnames(0) + if err != nil { + t.Fatal(err) + } + if len(names2) != 3 { + t.Fatalf("first names: %v, second names: %v", names1, names2) + } +} + +// isDeadlineExceeded reports whether err is or wraps ErrDeadlineExceeded. +// We also check that the error has a Timeout method that returns true. +func isDeadlineExceeded(err error) bool { + if !IsTimeout(err) { + return false + } + if !errors.Is(err, ErrDeadlineExceeded) { + return false + } + return true +} + +// Test that opening a file does not change its permissions. Issue 38225. +func TestOpenFileKeepsPermissions(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + name := filepath.Join(dir, "x") + f, err := Create(name) + if err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Error(err) + } + f, err = OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, 0) + if err != nil { + t.Fatal(err) + } + if fi, err := f.Stat(); err != nil { + t.Error(err) + } else if fi.Mode()&0222 == 0 { + t.Errorf("f.Stat.Mode after OpenFile is %v, should be writable", fi.Mode()) + } + if err := f.Close(); err != nil { + t.Error(err) + } + if fi, err := Stat(name); err != nil { + t.Error(err) + } else if fi.Mode()&0222 == 0 { + t.Errorf("Stat after OpenFile is %v, should be writable", fi.Mode()) + } +} + +func TestDirFS(t *testing.T) { + t.Parallel() + + // On Windows, we force the MFT to update by reading the actual metadata from GetFileInformationByHandle and then + // explicitly setting that. Otherwise it might get out of sync with FindFirstFile. See golang.org/issues/42637. + if runtime.GOOS == "windows" { + if err := filepath.WalkDir("./testdata/dirfs", func(path string, d fs.DirEntry, err error) error { + if err != nil { + t.Fatal(err) + } + info, err := d.Info() + if err != nil { + t.Fatal(err) + } + stat, err := Stat(path) // This uses GetFileInformationByHandle internally. + if err != nil { + t.Fatal(err) + } + if stat.ModTime() == info.ModTime() { + return nil + } + if err := Chtimes(path, stat.ModTime(), stat.ModTime()); err != nil { + t.Log(err) // We only log, not die, in case the test directory is not writable. + } + return nil + }); err != nil { + t.Fatal(err) + } + } + fsys := DirFS("./testdata/dirfs") + if err := fstest.TestFS(fsys, "a", "b", "dir/x"); err != nil { + t.Fatal(err) + } + + rdfs, ok := fsys.(fs.ReadDirFS) + if !ok { + t.Error("expected DirFS result to implement fs.ReadDirFS") + } + if _, err := rdfs.ReadDir("nonexistent"); err == nil { + t.Error("fs.ReadDir of nonexistent directory succeeded") + } + + // Test that the error message does not contain a backslash, + // and does not contain the DirFS argument. + const nonesuch = "dir/nonesuch" + _, err := fsys.Open(nonesuch) + if err == nil { + t.Error("fs.Open of nonexistent file succeeded") + } else { + if !strings.Contains(err.Error(), nonesuch) { + t.Errorf("error %q does not contain %q", err, nonesuch) + } + if strings.Contains(err.(*PathError).Path, "testdata") { + t.Errorf("error %q contains %q", err, "testdata") + } + } + + // Test that Open does not accept backslash as separator. + d := DirFS(".") + _, err = d.Open(`testdata\dirfs`) + if err == nil { + t.Fatalf(`Open testdata\dirfs succeeded`) + } + + // Test that Open does not open Windows device files. + _, err = d.Open(`NUL`) + if err == nil { + t.Errorf(`Open NUL succeeded`) + } +} + +func TestDirFSRootDir(t *testing.T) { + t.Parallel() + + cwd, err := Getwd() + if err != nil { + t.Fatal(err) + } + cwd = cwd[len(filepath.VolumeName(cwd)):] // trim volume prefix (C:) on Windows + cwd = filepath.ToSlash(cwd) // convert \ to / + cwd = strings.TrimPrefix(cwd, "/") // trim leading / + + // Test that Open can open a path starting at /. + d := DirFS("/") + f, err := d.Open(cwd + "/testdata/dirfs/a") + if err != nil { + t.Fatal(err) + } + f.Close() +} + +func TestDirFSEmptyDir(t *testing.T) { + t.Parallel() + + d := DirFS("") + cwd, _ := Getwd() + for _, path := range []string{ + "testdata/dirfs/a", // not DirFS(".") + filepath.ToSlash(cwd) + "/testdata/dirfs/a", // not DirFS("/") + } { + _, err := d.Open(path) + if err == nil { + t.Fatalf(`DirFS("").Open(%q) succeeded`, path) + } + } +} + +func TestDirFSPathsValid(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("skipping on Windows") + } + t.Parallel() + + d := t.TempDir() + if err := WriteFile(filepath.Join(d, "control.txt"), []byte(string("Hello, world!")), 0644); err != nil { + t.Fatal(err) + } + if err := WriteFile(filepath.Join(d, `e:xperi\ment.txt`), []byte(string("Hello, colon and backslash!")), 0644); err != nil { + t.Fatal(err) + } + + fsys := DirFS(d) + err := fs.WalkDir(fsys, ".", func(path string, e fs.DirEntry, err error) error { + if fs.ValidPath(e.Name()) { + t.Logf("%q ok", e.Name()) + } else { + t.Errorf("%q INVALID", e.Name()) + } + return nil + }) + if err != nil { + t.Fatal(err) + } +} + +func TestReadFileProc(t *testing.T) { + t.Parallel() + + // Linux files in /proc report 0 size, + // but then if ReadFile reads just a single byte at offset 0, + // the read at offset 1 returns EOF instead of more data. + // ReadFile has a minimum read size of 512 to work around this, + // but test explicitly that it's working. + name := "/proc/sys/fs/pipe-max-size" + if _, err := Stat(name); err != nil { + t.Skip(err) + } + data, err := ReadFile(name) + if err != nil { + t.Fatal(err) + } + if len(data) == 0 || data[len(data)-1] != '\n' { + t.Fatalf("read %s: not newline-terminated: %q", name, data) + } +} + +func TestDirFSReadFileProc(t *testing.T) { + t.Parallel() + + fsys := DirFS("/") + name := "proc/sys/fs/pipe-max-size" + if _, err := fs.Stat(fsys, name); err != nil { + t.Skip() + } + data, err := fs.ReadFile(fsys, name) + if err != nil { + t.Fatal(err) + } + if len(data) == 0 || data[len(data)-1] != '\n' { + t.Fatalf("read %s: not newline-terminated: %q", name, data) + } +} + +func TestWriteStringAlloc(t *testing.T) { + if runtime.GOOS == "js" { + t.Skip("js allocates a lot during File.WriteString") + } + d := t.TempDir() + f, err := Create(filepath.Join(d, "whiteboard.txt")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + allocs := testing.AllocsPerRun(100, func() { + f.WriteString("I will not allocate when passed a string longer than 32 bytes.\n") + }) + if allocs != 0 { + t.Errorf("expected 0 allocs for File.WriteString, got %v", allocs) + } +} + +// Test that it's OK to have parallel I/O and Close on a pipe. +func TestPipeIOCloseRace(t *testing.T) { + // Skip on wasm, which doesn't have pipes. + if runtime.GOOS == "js" || runtime.GOOS == "wasip1" { + t.Skipf("skipping on %s: no pipes", runtime.GOOS) + } + t.Parallel() + + r, w, err := Pipe() + if err != nil { + t.Fatal(err) + } + + var wg sync.WaitGroup + wg.Add(3) + + go func() { + defer wg.Done() + for { + n, err := w.Write([]byte("hi")) + if err != nil { + // We look at error strings as the + // expected errors are OS-specific. + switch { + case errors.Is(err, ErrClosed), + strings.Contains(err.Error(), "broken pipe"), + strings.Contains(err.Error(), "pipe is being closed"), + strings.Contains(err.Error(), "hungup channel"): + // Ignore an expected error. + default: + // Unexpected error. + t.Error(err) + } + return + } + if n != 2 { + t.Errorf("wrote %d bytes, expected 2", n) + return + } + } + }() + + go func() { + defer wg.Done() + for { + var buf [2]byte + n, err := r.Read(buf[:]) + if err != nil { + if err != io.EOF && !errors.Is(err, ErrClosed) { + t.Error(err) + } + return + } + if n != 2 { + t.Errorf("read %d bytes, want 2", n) + } + } + }() + + go func() { + defer wg.Done() + + // Let the other goroutines start. This is just to get + // a better test, the test will still pass if they + // don't start. + time.Sleep(time.Millisecond) + + if err := r.Close(); err != nil { + t.Error(err) + } + if err := w.Close(); err != nil { + t.Error(err) + } + }() + + wg.Wait() +} + +// Test that it's OK to call Close concurrently on a pipe. +func TestPipeCloseRace(t *testing.T) { + // Skip on wasm, which doesn't have pipes. + if runtime.GOOS == "js" || runtime.GOOS == "wasip1" { + t.Skipf("skipping on %s: no pipes", runtime.GOOS) + } + t.Parallel() + + r, w, err := Pipe() + if err != nil { + t.Fatal(err) + } + var wg sync.WaitGroup + c := make(chan error, 4) + f := func() { + defer wg.Done() + c <- r.Close() + c <- w.Close() + } + wg.Add(2) + go f() + go f() + nils, errs := 0, 0 + for i := 0; i < 4; i++ { + err := <-c + if err == nil { + nils++ + } else { + errs++ + } + } + if nils != 2 || errs != 2 { + t.Errorf("got nils %d errs %d, want 2 2", nils, errs) + } +} diff --git a/src/os/os_unix_test.go b/src/os/os_unix_test.go new file mode 100644 index 0000000..98e7afd --- /dev/null +++ b/src/os/os_unix_test.go @@ -0,0 +1,348 @@ +// Copyright 2009 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. + +//go:build unix || (js && wasm) || wasip1 + +package os_test + +import ( + "internal/testenv" + "io" + . "os" + "path/filepath" + "runtime" + "strings" + "syscall" + "testing" + "time" +) + +func init() { + isReadonlyError = func(err error) bool { return err == syscall.EROFS } +} + +// For TestRawConnReadWrite. +type syscallDescriptor = int + +func checkUidGid(t *testing.T, path string, uid, gid int) { + dir, err := Lstat(path) + if err != nil { + t.Fatalf("Lstat %q (looking for uid/gid %d/%d): %s", path, uid, gid, err) + } + sys := dir.Sys().(*syscall.Stat_t) + if int(sys.Uid) != uid { + t.Errorf("Lstat %q: uid %d want %d", path, sys.Uid, uid) + } + if int(sys.Gid) != gid { + t.Errorf("Lstat %q: gid %d want %d", path, sys.Gid, gid) + } +} + +func TestChown(t *testing.T) { + if runtime.GOOS == "wasip1" { + t.Skip("file ownership not supported on " + runtime.GOOS) + } + t.Parallel() + + // Use TempDir() to make sure we're on a local file system, + // so that the group ids returned by Getgroups will be allowed + // on the file. On NFS, the Getgroups groups are + // basically useless. + f := newFile("TestChown", t) + defer Remove(f.Name()) + defer f.Close() + dir, err := f.Stat() + if err != nil { + t.Fatalf("stat %s: %s", f.Name(), err) + } + + // Can't change uid unless root, but can try + // changing the group id. First try our current group. + gid := Getgid() + t.Log("gid:", gid) + if err = Chown(f.Name(), -1, gid); err != nil { + t.Fatalf("chown %s -1 %d: %s", f.Name(), gid, err) + } + sys := dir.Sys().(*syscall.Stat_t) + checkUidGid(t, f.Name(), int(sys.Uid), gid) + + // Then try all the auxiliary groups. + groups, err := Getgroups() + if err != nil { + t.Fatalf("getgroups: %s", err) + } + t.Log("groups: ", groups) + for _, g := range groups { + if err = Chown(f.Name(), -1, g); err != nil { + t.Fatalf("chown %s -1 %d: %s", f.Name(), g, err) + } + checkUidGid(t, f.Name(), int(sys.Uid), g) + + // change back to gid to test fd.Chown + if err = f.Chown(-1, gid); err != nil { + t.Fatalf("fchown %s -1 %d: %s", f.Name(), gid, err) + } + checkUidGid(t, f.Name(), int(sys.Uid), gid) + } +} + +func TestFileChown(t *testing.T) { + if runtime.GOOS == "wasip1" { + t.Skip("file ownership not supported on " + runtime.GOOS) + } + t.Parallel() + + // Use TempDir() to make sure we're on a local file system, + // so that the group ids returned by Getgroups will be allowed + // on the file. On NFS, the Getgroups groups are + // basically useless. + f := newFile("TestFileChown", t) + defer Remove(f.Name()) + defer f.Close() + dir, err := f.Stat() + if err != nil { + t.Fatalf("stat %s: %s", f.Name(), err) + } + + // Can't change uid unless root, but can try + // changing the group id. First try our current group. + gid := Getgid() + t.Log("gid:", gid) + if err = f.Chown(-1, gid); err != nil { + t.Fatalf("fchown %s -1 %d: %s", f.Name(), gid, err) + } + sys := dir.Sys().(*syscall.Stat_t) + checkUidGid(t, f.Name(), int(sys.Uid), gid) + + // Then try all the auxiliary groups. + groups, err := Getgroups() + if err != nil { + t.Fatalf("getgroups: %s", err) + } + t.Log("groups: ", groups) + for _, g := range groups { + if err = f.Chown(-1, g); err != nil { + t.Fatalf("fchown %s -1 %d: %s", f.Name(), g, err) + } + checkUidGid(t, f.Name(), int(sys.Uid), g) + + // change back to gid to test fd.Chown + if err = f.Chown(-1, gid); err != nil { + t.Fatalf("fchown %s -1 %d: %s", f.Name(), gid, err) + } + checkUidGid(t, f.Name(), int(sys.Uid), gid) + } +} + +func TestLchown(t *testing.T) { + testenv.MustHaveSymlink(t) + t.Parallel() + + // Use TempDir() to make sure we're on a local file system, + // so that the group ids returned by Getgroups will be allowed + // on the file. On NFS, the Getgroups groups are + // basically useless. + f := newFile("TestLchown", t) + defer Remove(f.Name()) + defer f.Close() + dir, err := f.Stat() + if err != nil { + t.Fatalf("stat %s: %s", f.Name(), err) + } + + linkname := f.Name() + "2" + if err := Symlink(f.Name(), linkname); err != nil { + if runtime.GOOS == "android" && IsPermission(err) { + t.Skip("skipping test on Android; permission error creating symlink") + } + t.Fatalf("link %s -> %s: %v", f.Name(), linkname, err) + } + defer Remove(linkname) + + // Can't change uid unless root, but can try + // changing the group id. First try our current group. + gid := Getgid() + t.Log("gid:", gid) + if err = Lchown(linkname, -1, gid); err != nil { + if err, ok := err.(*PathError); ok && err.Err == syscall.ENOSYS { + t.Skip("lchown is unavailable") + } + t.Fatalf("lchown %s -1 %d: %s", linkname, gid, err) + } + sys := dir.Sys().(*syscall.Stat_t) + checkUidGid(t, linkname, int(sys.Uid), gid) + + // Then try all the auxiliary groups. + groups, err := Getgroups() + if err != nil { + t.Fatalf("getgroups: %s", err) + } + t.Log("groups: ", groups) + for _, g := range groups { + if err = Lchown(linkname, -1, g); err != nil { + t.Fatalf("lchown %s -1 %d: %s", linkname, g, err) + } + checkUidGid(t, linkname, int(sys.Uid), g) + + // Check that link target's gid is unchanged. + checkUidGid(t, f.Name(), int(sys.Uid), int(sys.Gid)) + } +} + +// Issue 16919: Readdir must return a non-empty slice or an error. +func TestReaddirRemoveRace(t *testing.T) { + oldStat := *LstatP + defer func() { *LstatP = oldStat }() + *LstatP = func(name string) (FileInfo, error) { + if strings.HasSuffix(name, "some-file") { + // Act like it's been deleted. + return nil, ErrNotExist + } + return oldStat(name) + } + dir := newDir("TestReaddirRemoveRace", t) + defer RemoveAll(dir) + if err := WriteFile(filepath.Join(dir, "some-file"), []byte("hello"), 0644); err != nil { + t.Fatal(err) + } + d, err := Open(dir) + if err != nil { + t.Fatal(err) + } + defer d.Close() + fis, err := d.Readdir(2) // notably, greater than zero + if len(fis) == 0 && err == nil { + // This is what used to happen (Issue 16919) + t.Fatal("Readdir = empty slice & err == nil") + } + if len(fis) != 0 || err != io.EOF { + t.Errorf("Readdir = %d entries: %v; want 0, io.EOF", len(fis), err) + for i, fi := range fis { + t.Errorf(" entry[%d]: %q, %v", i, fi.Name(), fi.Mode()) + } + t.FailNow() + } +} + +// Issue 23120: respect umask when doing Mkdir with the sticky bit +func TestMkdirStickyUmask(t *testing.T) { + if runtime.GOOS == "wasip1" { + t.Skip("file permissions not supported on " + runtime.GOOS) + } + t.Parallel() + + const umask = 0077 + dir := newDir("TestMkdirStickyUmask", t) + defer RemoveAll(dir) + oldUmask := syscall.Umask(umask) + defer syscall.Umask(oldUmask) + p := filepath.Join(dir, "dir1") + if err := Mkdir(p, ModeSticky|0755); err != nil { + t.Fatal(err) + } + fi, err := Stat(p) + if err != nil { + t.Fatal(err) + } + if mode := fi.Mode(); (mode&umask) != 0 || (mode&^ModePerm) != (ModeDir|ModeSticky) { + t.Errorf("unexpected mode %s", mode) + } +} + +// See also issues: 22939, 24331 +func newFileTest(t *testing.T, blocking bool) { + if runtime.GOOS == "js" || runtime.GOOS == "wasip1" { + t.Skipf("syscall.Pipe is not available on %s.", runtime.GOOS) + } + + p := make([]int, 2) + if err := syscall.Pipe(p); err != nil { + t.Fatalf("pipe: %v", err) + } + defer syscall.Close(p[1]) + + // Set the read-side to non-blocking. + if !blocking { + if err := syscall.SetNonblock(p[0], true); err != nil { + syscall.Close(p[0]) + t.Fatalf("SetNonblock: %v", err) + } + } + // Convert it to a file. + file := NewFile(uintptr(p[0]), "notapipe") + if file == nil { + syscall.Close(p[0]) + t.Fatalf("failed to convert fd to file!") + } + defer file.Close() + + timeToWrite := 100 * time.Millisecond + timeToDeadline := 1 * time.Millisecond + if !blocking { + // Use a longer time to avoid flakes. + // We won't be waiting this long anyhow. + timeToWrite = 1 * time.Second + } + + // Try to read with deadline (but don't block forever). + b := make([]byte, 1) + timer := time.AfterFunc(timeToWrite, func() { syscall.Write(p[1], []byte("a")) }) + defer timer.Stop() + file.SetReadDeadline(time.Now().Add(timeToDeadline)) + _, err := file.Read(b) + if !blocking { + // We want it to fail with a timeout. + if !isDeadlineExceeded(err) { + t.Fatalf("No timeout reading from file: %v", err) + } + } else { + // We want it to succeed after 100ms + if err != nil { + t.Fatalf("Error reading from file: %v", err) + } + } +} + +func TestNewFileBlock(t *testing.T) { + t.Parallel() + newFileTest(t, true) +} + +func TestNewFileNonBlock(t *testing.T) { + t.Parallel() + newFileTest(t, false) +} + +func TestNewFileInvalid(t *testing.T) { + t.Parallel() + const negOne = ^uintptr(0) + if f := NewFile(negOne, "invalid"); f != nil { + t.Errorf("NewFile(-1) got %v want nil", f) + } +} + +func TestSplitPath(t *testing.T) { + t.Parallel() + for _, tt := range []struct{ path, wantDir, wantBase string }{ + {"a", ".", "a"}, + {"a/", ".", "a"}, + {"a//", ".", "a"}, + {"a/b", "a", "b"}, + {"a/b/", "a", "b"}, + {"a/b/c", "a/b", "c"}, + {"/a", "/", "a"}, + {"/a/", "/", "a"}, + {"/a/b", "/a", "b"}, + {"/a/b/", "/a", "b"}, + {"/a/b/c", "/a/b", "c"}, + {"//a", "/", "a"}, + {"//a/", "/", "a"}, + {"///a", "/", "a"}, + {"///a/", "/", "a"}, + } { + if dir, base := SplitPath(tt.path); dir != tt.wantDir || base != tt.wantBase { + t.Errorf("splitPath(%q) = %q, %q, want %q, %q", tt.path, dir, base, tt.wantDir, tt.wantBase) + } + } +} diff --git a/src/os/os_windows_test.go b/src/os/os_windows_test.go new file mode 100644 index 0000000..d6aab18 --- /dev/null +++ b/src/os/os_windows_test.go @@ -0,0 +1,1467 @@ +// Copyright 2014 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 os_test + +import ( + "errors" + "fmt" + "internal/poll" + "internal/syscall/windows" + "internal/syscall/windows/registry" + "internal/testenv" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "reflect" + "runtime" + "slices" + "sort" + "strings" + "syscall" + "testing" + "unicode/utf16" + "unsafe" +) + +// For TestRawConnReadWrite. +type syscallDescriptor = syscall.Handle + +// chdir changes the current working directory to the named directory, +// and then restore the original working directory at the end of the test. +func chdir(t *testing.T, dir string) { + olddir, err := os.Getwd() + if err != nil { + t.Fatalf("chdir: %v", err) + } + if err := os.Chdir(dir); err != nil { + t.Fatalf("chdir %s: %v", dir, err) + } + + t.Cleanup(func() { + if err := os.Chdir(olddir); err != nil { + t.Errorf("chdir to original working directory %s: %v", olddir, err) + os.Exit(1) + } + }) +} + +func TestSameWindowsFile(t *testing.T) { + temp := t.TempDir() + chdir(t, temp) + + f, err := os.Create("a") + if err != nil { + t.Fatal(err) + } + f.Close() + + ia1, err := os.Stat("a") + if err != nil { + t.Fatal(err) + } + + path, err := filepath.Abs("a") + if err != nil { + t.Fatal(err) + } + ia2, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if !os.SameFile(ia1, ia2) { + t.Errorf("files should be same") + } + + p := filepath.VolumeName(path) + filepath.Base(path) + if err != nil { + t.Fatal(err) + } + ia3, err := os.Stat(p) + if err != nil { + t.Fatal(err) + } + if !os.SameFile(ia1, ia3) { + t.Errorf("files should be same") + } +} + +type dirLinkTest struct { + name string + mklink func(link, target string) error + issueNo int // correspondent issue number (for broken tests) +} + +func testDirLinks(t *testing.T, tests []dirLinkTest) { + tmpdir := t.TempDir() + chdir(t, tmpdir) + + dir := filepath.Join(tmpdir, "dir") + err := os.Mkdir(dir, 0777) + if err != nil { + t.Fatal(err) + } + fi, err := os.Stat(dir) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(filepath.Join(dir, "abc"), []byte("abc"), 0644) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + link := filepath.Join(tmpdir, test.name+"_link") + err := test.mklink(link, dir) + if err != nil { + t.Errorf("creating link for %q test failed: %v", test.name, err) + continue + } + + data, err := os.ReadFile(filepath.Join(link, "abc")) + if err != nil { + t.Errorf("failed to read abc file: %v", err) + continue + } + if string(data) != "abc" { + t.Errorf(`abc file is expected to have "abc" in it, but has %v`, data) + continue + } + + if test.issueNo > 0 { + t.Logf("skipping broken %q test: see issue %d", test.name, test.issueNo) + continue + } + + fi1, err := os.Stat(link) + if err != nil { + t.Errorf("failed to stat link %v: %v", link, err) + continue + } + if !fi1.IsDir() { + t.Errorf("%q should be a directory", link) + continue + } + if fi1.Name() != filepath.Base(link) { + t.Errorf("Stat(%q).Name() = %q, want %q", link, fi1.Name(), filepath.Base(link)) + continue + } + if !os.SameFile(fi, fi1) { + t.Errorf("%q should point to %q", link, dir) + continue + } + + fi2, err := os.Lstat(link) + if err != nil { + t.Errorf("failed to lstat link %v: %v", link, err) + continue + } + if m := fi2.Mode(); m&fs.ModeSymlink == 0 { + t.Errorf("%q should be a link, but is not (mode=0x%x)", link, uint32(m)) + continue + } + if m := fi2.Mode(); m&fs.ModeDir != 0 { + t.Errorf("%q should be a link, not a directory (mode=0x%x)", link, uint32(m)) + continue + } + } +} + +// reparseData is used to build reparse buffer data required for tests. +type reparseData struct { + substituteName namePosition + printName namePosition + pathBuf []uint16 +} + +type namePosition struct { + offset uint16 + length uint16 +} + +func (rd *reparseData) addUTF16s(s []uint16) (offset uint16) { + off := len(rd.pathBuf) * 2 + rd.pathBuf = append(rd.pathBuf, s...) + return uint16(off) +} + +func (rd *reparseData) addString(s string) (offset, length uint16) { + p := syscall.StringToUTF16(s) + return rd.addUTF16s(p), uint16(len(p)-1) * 2 // do not include terminating NUL in the length (as per PrintNameLength and SubstituteNameLength documentation) +} + +func (rd *reparseData) addSubstituteName(name string) { + rd.substituteName.offset, rd.substituteName.length = rd.addString(name) +} + +func (rd *reparseData) addPrintName(name string) { + rd.printName.offset, rd.printName.length = rd.addString(name) +} + +func (rd *reparseData) addStringNoNUL(s string) (offset, length uint16) { + p := syscall.StringToUTF16(s) + p = p[:len(p)-1] + return rd.addUTF16s(p), uint16(len(p)) * 2 +} + +func (rd *reparseData) addSubstituteNameNoNUL(name string) { + rd.substituteName.offset, rd.substituteName.length = rd.addStringNoNUL(name) +} + +func (rd *reparseData) addPrintNameNoNUL(name string) { + rd.printName.offset, rd.printName.length = rd.addStringNoNUL(name) +} + +// pathBuffeLen returns length of rd pathBuf in bytes. +func (rd *reparseData) pathBuffeLen() uint16 { + return uint16(len(rd.pathBuf)) * 2 +} + +// Windows REPARSE_DATA_BUFFER contains union member, and cannot be +// translated into Go directly. _REPARSE_DATA_BUFFER type is to help +// construct alternative versions of Windows REPARSE_DATA_BUFFER with +// union part of SymbolicLinkReparseBuffer or MountPointReparseBuffer type. +type _REPARSE_DATA_BUFFER struct { + header windows.REPARSE_DATA_BUFFER_HEADER + detail [syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]byte +} + +func createDirLink(link string, rdb *_REPARSE_DATA_BUFFER) error { + err := os.Mkdir(link, 0777) + if err != nil { + return err + } + + linkp := syscall.StringToUTF16(link) + fd, err := syscall.CreateFile(&linkp[0], syscall.GENERIC_WRITE, 0, nil, syscall.OPEN_EXISTING, + syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0) + if err != nil { + return err + } + defer syscall.CloseHandle(fd) + + buflen := uint32(rdb.header.ReparseDataLength) + uint32(unsafe.Sizeof(rdb.header)) + var bytesReturned uint32 + return syscall.DeviceIoControl(fd, windows.FSCTL_SET_REPARSE_POINT, + (*byte)(unsafe.Pointer(&rdb.header)), buflen, nil, 0, &bytesReturned, nil) +} + +func createMountPoint(link string, target *reparseData) error { + var buf *windows.MountPointReparseBuffer + buflen := uint16(unsafe.Offsetof(buf.PathBuffer)) + target.pathBuffeLen() // see ReparseDataLength documentation + byteblob := make([]byte, buflen) + buf = (*windows.MountPointReparseBuffer)(unsafe.Pointer(&byteblob[0])) + buf.SubstituteNameOffset = target.substituteName.offset + buf.SubstituteNameLength = target.substituteName.length + buf.PrintNameOffset = target.printName.offset + buf.PrintNameLength = target.printName.length + pbuflen := len(target.pathBuf) + copy((*[2048]uint16)(unsafe.Pointer(&buf.PathBuffer[0]))[:pbuflen:pbuflen], target.pathBuf) + + var rdb _REPARSE_DATA_BUFFER + rdb.header.ReparseTag = windows.IO_REPARSE_TAG_MOUNT_POINT + rdb.header.ReparseDataLength = buflen + copy(rdb.detail[:], byteblob) + + return createDirLink(link, &rdb) +} + +func TestDirectoryJunction(t *testing.T) { + var tests = []dirLinkTest{ + { + // Create link similar to what mklink does, by inserting \??\ at the front of absolute target. + name: "standard", + mklink: func(link, target string) error { + var t reparseData + t.addSubstituteName(`\??\` + target) + t.addPrintName(target) + return createMountPoint(link, &t) + }, + }, + { + // Do as junction utility https://technet.microsoft.com/en-au/sysinternals/bb896768.aspx does - set PrintNameLength to 0. + name: "have_blank_print_name", + mklink: func(link, target string) error { + var t reparseData + t.addSubstituteName(`\??\` + target) + t.addPrintName("") + return createMountPoint(link, &t) + }, + }, + } + output, _ := testenv.Command(t, "cmd", "/c", "mklink", "/?").Output() + mklinkSupportsJunctionLinks := strings.Contains(string(output), " /J ") + if mklinkSupportsJunctionLinks { + tests = append(tests, + dirLinkTest{ + name: "use_mklink_cmd", + mklink: func(link, target string) error { + output, err := testenv.Command(t, "cmd", "/c", "mklink", "/J", link, target).CombinedOutput() + if err != nil { + t.Errorf("failed to run mklink %v %v: %v %q", link, target, err, output) + } + return nil + }, + }, + ) + } else { + t.Log(`skipping "use_mklink_cmd" test, mklink does not supports directory junctions`) + } + testDirLinks(t, tests) +} + +func enableCurrentThreadPrivilege(privilegeName string) error { + ct, err := windows.GetCurrentThread() + if err != nil { + return err + } + var t syscall.Token + err = windows.OpenThreadToken(ct, syscall.TOKEN_QUERY|windows.TOKEN_ADJUST_PRIVILEGES, false, &t) + if err != nil { + return err + } + defer syscall.CloseHandle(syscall.Handle(t)) + + var tp windows.TOKEN_PRIVILEGES + + privStr, err := syscall.UTF16PtrFromString(privilegeName) + if err != nil { + return err + } + err = windows.LookupPrivilegeValue(nil, privStr, &tp.Privileges[0].Luid) + if err != nil { + return err + } + tp.PrivilegeCount = 1 + tp.Privileges[0].Attributes = windows.SE_PRIVILEGE_ENABLED + return windows.AdjustTokenPrivileges(t, false, &tp, 0, nil, nil) +} + +func createSymbolicLink(link string, target *reparseData, isrelative bool) error { + var buf *windows.SymbolicLinkReparseBuffer + buflen := uint16(unsafe.Offsetof(buf.PathBuffer)) + target.pathBuffeLen() // see ReparseDataLength documentation + byteblob := make([]byte, buflen) + buf = (*windows.SymbolicLinkReparseBuffer)(unsafe.Pointer(&byteblob[0])) + buf.SubstituteNameOffset = target.substituteName.offset + buf.SubstituteNameLength = target.substituteName.length + buf.PrintNameOffset = target.printName.offset + buf.PrintNameLength = target.printName.length + if isrelative { + buf.Flags = windows.SYMLINK_FLAG_RELATIVE + } + pbuflen := len(target.pathBuf) + copy((*[2048]uint16)(unsafe.Pointer(&buf.PathBuffer[0]))[:pbuflen:pbuflen], target.pathBuf) + + var rdb _REPARSE_DATA_BUFFER + rdb.header.ReparseTag = syscall.IO_REPARSE_TAG_SYMLINK + rdb.header.ReparseDataLength = buflen + copy(rdb.detail[:], byteblob) + + return createDirLink(link, &rdb) +} + +func TestDirectorySymbolicLink(t *testing.T) { + var tests []dirLinkTest + output, _ := testenv.Command(t, "cmd", "/c", "mklink", "/?").Output() + mklinkSupportsDirectorySymbolicLinks := strings.Contains(string(output), " /D ") + if mklinkSupportsDirectorySymbolicLinks { + tests = append(tests, + dirLinkTest{ + name: "use_mklink_cmd", + mklink: func(link, target string) error { + output, err := testenv.Command(t, "cmd", "/c", "mklink", "/D", link, target).CombinedOutput() + if err != nil { + t.Errorf("failed to run mklink %v %v: %v %q", link, target, err, output) + } + return nil + }, + }, + ) + } else { + t.Log(`skipping "use_mklink_cmd" test, mklink does not supports directory symbolic links`) + } + + // The rest of these test requires SeCreateSymbolicLinkPrivilege to be held. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := windows.ImpersonateSelf(windows.SecurityImpersonation) + if err != nil { + t.Fatal(err) + } + defer windows.RevertToSelf() + + err = enableCurrentThreadPrivilege("SeCreateSymbolicLinkPrivilege") + if err != nil { + t.Skipf(`skipping some tests, could not enable "SeCreateSymbolicLinkPrivilege": %v`, err) + } + tests = append(tests, + dirLinkTest{ + name: "use_os_pkg", + mklink: func(link, target string) error { + return os.Symlink(target, link) + }, + }, + dirLinkTest{ + // Create link similar to what mklink does, by inserting \??\ at the front of absolute target. + name: "standard", + mklink: func(link, target string) error { + var t reparseData + t.addPrintName(target) + t.addSubstituteName(`\??\` + target) + return createSymbolicLink(link, &t, false) + }, + }, + dirLinkTest{ + name: "relative", + mklink: func(link, target string) error { + var t reparseData + t.addSubstituteNameNoNUL(filepath.Base(target)) + t.addPrintNameNoNUL(filepath.Base(target)) + return createSymbolicLink(link, &t, true) + }, + }, + ) + testDirLinks(t, tests) +} + +func TestNetworkSymbolicLink(t *testing.T) { + testenv.MustHaveSymlink(t) + + const _NERR_ServerNotStarted = syscall.Errno(2114) + + dir := t.TempDir() + chdir(t, dir) + + shareName := "GoSymbolicLinkTestShare" // hope no conflictions + sharePath := filepath.Join(dir, shareName) + testDir := "TestDir" + + err := os.MkdirAll(filepath.Join(sharePath, testDir), 0777) + if err != nil { + t.Fatal(err) + } + + wShareName, err := syscall.UTF16PtrFromString(shareName) + if err != nil { + t.Fatal(err) + } + wSharePath, err := syscall.UTF16PtrFromString(sharePath) + if err != nil { + t.Fatal(err) + } + + p := windows.SHARE_INFO_2{ + Netname: wShareName, + Type: windows.STYPE_DISKTREE, + Remark: nil, + Permissions: 0, + MaxUses: 1, + CurrentUses: 0, + Path: wSharePath, + Passwd: nil, + } + + err = windows.NetShareAdd(nil, 2, (*byte)(unsafe.Pointer(&p)), nil) + if err != nil { + if err == syscall.ERROR_ACCESS_DENIED { + t.Skip("you don't have enough privileges to add network share") + } + if err == _NERR_ServerNotStarted { + t.Skip(_NERR_ServerNotStarted.Error()) + } + t.Fatal(err) + } + defer func() { + err := windows.NetShareDel(nil, wShareName, 0) + if err != nil { + t.Fatal(err) + } + }() + + UNCPath := `\\localhost\` + shareName + `\` + + fi1, err := os.Stat(sharePath) + if err != nil { + t.Fatal(err) + } + fi2, err := os.Stat(UNCPath) + if err != nil { + t.Fatal(err) + } + if !os.SameFile(fi1, fi2) { + t.Fatalf("%q and %q should be the same directory, but not", sharePath, UNCPath) + } + + target := filepath.Join(UNCPath, testDir) + link := "link" + + err = os.Symlink(target, link) + if err != nil { + t.Fatal(err) + } + defer os.Remove(link) + + got, err := os.Readlink(link) + if err != nil { + t.Fatal(err) + } + if got != target { + t.Errorf(`os.Readlink("%s"): got %v, want %v`, link, got, target) + } + + got, err = filepath.EvalSymlinks(link) + if err != nil { + t.Fatal(err) + } + if got != target { + t.Errorf(`filepath.EvalSymlinks("%s"): got %v, want %v`, link, got, target) + } +} + +func TestStartProcessAttr(t *testing.T) { + t.Parallel() + + p, err := os.StartProcess(os.Getenv("COMSPEC"), []string{"/c", "cd"}, new(os.ProcAttr)) + if err != nil { + return + } + defer p.Wait() + t.Fatalf("StartProcess expected to fail, but succeeded.") +} + +func TestShareNotExistError(t *testing.T) { + if testing.Short() { + t.Skip("slow test that uses network; skipping") + } + t.Parallel() + + _, err := os.Stat(`\\no_such_server\no_such_share\no_such_file`) + if err == nil { + t.Fatal("stat succeeded, but expected to fail") + } + if !os.IsNotExist(err) { + t.Fatalf("os.Stat failed with %q, but os.IsNotExist(err) is false", err) + } +} + +func TestBadNetPathError(t *testing.T) { + const ERROR_BAD_NETPATH = syscall.Errno(53) + if !os.IsNotExist(ERROR_BAD_NETPATH) { + t.Fatal("os.IsNotExist(syscall.Errno(53)) is false, but want true") + } +} + +func TestStatDir(t *testing.T) { + defer chtmpdir(t)() + + f, err := os.Open(".") + if err != nil { + t.Fatal(err) + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + t.Fatal(err) + } + + err = os.Chdir("..") + if err != nil { + t.Fatal(err) + } + + fi2, err := f.Stat() + if err != nil { + t.Fatal(err) + } + + if !os.SameFile(fi, fi2) { + t.Fatal("race condition occurred") + } +} + +func TestOpenVolumeName(t *testing.T) { + tmpdir := t.TempDir() + chdir(t, tmpdir) + + want := []string{"file1", "file2", "file3", "gopher.txt"} + sort.Strings(want) + for _, name := range want { + err := os.WriteFile(filepath.Join(tmpdir, name), nil, 0777) + if err != nil { + t.Fatal(err) + } + } + + f, err := os.Open(filepath.VolumeName(tmpdir)) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + have, err := f.Readdirnames(-1) + if err != nil { + t.Fatal(err) + } + sort.Strings(have) + + if strings.Join(want, "/") != strings.Join(have, "/") { + t.Fatalf("unexpected file list %q, want %q", have, want) + } +} + +func TestDeleteReadOnly(t *testing.T) { + t.Parallel() + + tmpdir := t.TempDir() + p := filepath.Join(tmpdir, "a") + // This sets FILE_ATTRIBUTE_READONLY. + f, err := os.OpenFile(p, os.O_CREATE, 0400) + if err != nil { + t.Fatal(err) + } + f.Close() + + if err = os.Chmod(p, 0400); err != nil { + t.Fatal(err) + } + if err = os.Remove(p); err != nil { + t.Fatal(err) + } +} + +func TestReadStdin(t *testing.T) { + old := poll.ReadConsole + defer func() { + poll.ReadConsole = old + }() + + p, err := syscall.GetCurrentProcess() + if err != nil { + t.Fatalf("Unable to get handle to current process: %v", err) + } + var stdinDuplicate syscall.Handle + err = syscall.DuplicateHandle(p, syscall.Handle(syscall.Stdin), p, &stdinDuplicate, 0, false, syscall.DUPLICATE_SAME_ACCESS) + if err != nil { + t.Fatalf("Unable to duplicate stdin: %v", err) + } + testConsole := os.NewConsoleFile(stdinDuplicate, "test") + + var tests = []string{ + "abc", + "äöü", + "\u3042", + "“hi”™", + "hello\x1aworld", + "\U0001F648\U0001F649\U0001F64A", + } + + for _, consoleSize := range []int{1, 2, 3, 10, 16, 100, 1000} { + for _, readSize := range []int{1, 2, 3, 4, 5, 8, 10, 16, 20, 50, 100} { + for _, s := range tests { + t.Run(fmt.Sprintf("c%d/r%d/%s", consoleSize, readSize, s), func(t *testing.T) { + s16 := utf16.Encode([]rune(s)) + poll.ReadConsole = func(h syscall.Handle, buf *uint16, toread uint32, read *uint32, inputControl *byte) error { + if inputControl != nil { + t.Fatalf("inputControl not nil") + } + n := int(toread) + if n > consoleSize { + n = consoleSize + } + n = copy((*[10000]uint16)(unsafe.Pointer(buf))[:n:n], s16) + s16 = s16[n:] + *read = uint32(n) + t.Logf("read %d -> %d", toread, *read) + return nil + } + + var all []string + var buf []byte + chunk := make([]byte, readSize) + for { + n, err := testConsole.Read(chunk) + buf = append(buf, chunk[:n]...) + if err == io.EOF { + all = append(all, string(buf)) + if len(all) >= 5 { + break + } + buf = buf[:0] + } else if err != nil { + t.Fatalf("reading %q: error: %v", s, err) + } + if len(buf) >= 2000 { + t.Fatalf("reading %q: stuck in loop: %q", s, buf) + } + } + + want := strings.Split(s, "\x1a") + for len(want) < 5 { + want = append(want, "") + } + if !reflect.DeepEqual(all, want) { + t.Errorf("reading %q:\nhave %x\nwant %x", s, all, want) + } + }) + } + } + } +} + +func TestStatPagefile(t *testing.T) { + t.Parallel() + + const path = `c:\pagefile.sys` + fi, err := os.Stat(path) + if err == nil { + if fi.Name() == "" { + t.Fatalf("Stat(%q).Name() is empty", path) + } + t.Logf("Stat(%q).Size() = %v", path, fi.Size()) + return + } + if os.IsNotExist(err) { + t.Skip(`skipping because c:\pagefile.sys is not found`) + } + t.Fatal(err) +} + +// syscallCommandLineToArgv calls syscall.CommandLineToArgv +// and converts returned result into []string. +func syscallCommandLineToArgv(cmd string) ([]string, error) { + var argc int32 + argv, err := syscall.CommandLineToArgv(&syscall.StringToUTF16(cmd)[0], &argc) + if err != nil { + return nil, err + } + defer syscall.LocalFree(syscall.Handle(uintptr(unsafe.Pointer(argv)))) + + var args []string + for _, v := range (*argv)[:argc] { + args = append(args, syscall.UTF16ToString((*v)[:])) + } + return args, nil +} + +// compareCommandLineToArgvWithSyscall ensures that +// os.CommandLineToArgv(cmd) and syscall.CommandLineToArgv(cmd) +// return the same result. +func compareCommandLineToArgvWithSyscall(t *testing.T, cmd string) { + syscallArgs, err := syscallCommandLineToArgv(cmd) + if err != nil { + t.Fatal(err) + } + args := os.CommandLineToArgv(cmd) + if want, have := fmt.Sprintf("%q", syscallArgs), fmt.Sprintf("%q", args); want != have { + t.Errorf("testing os.commandLineToArgv(%q) failed: have %q want %q", cmd, args, syscallArgs) + return + } +} + +func TestCmdArgs(t *testing.T) { + if testing.Short() { + t.Skipf("in short mode; skipping test that builds a binary") + } + t.Parallel() + + tmpdir := t.TempDir() + + const prog = ` +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Printf("%q", os.Args) +} +` + src := filepath.Join(tmpdir, "main.go") + if err := os.WriteFile(src, []byte(prog), 0666); err != nil { + t.Fatal(err) + } + + exe := filepath.Join(tmpdir, "main.exe") + cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", exe, src) + cmd.Dir = tmpdir + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("building main.exe failed: %v\n%s", err, out) + } + + var cmds = []string{ + ``, + ` a b c`, + ` "`, + ` ""`, + ` """`, + ` "" a`, + ` "123"`, + ` \"123\"`, + ` \"123 456\"`, + ` \\"`, + ` \\\"`, + ` \\\\\"`, + ` \\\"x`, + ` """"\""\\\"`, + ` abc`, + ` \\\\\""x"""y z`, + "\tb\t\"x\ty\"", + ` "Брад" d e`, + // examples from https://msdn.microsoft.com/en-us/library/17w5ykft.aspx + ` "abc" d e`, + ` a\\b d"e f"g h`, + ` a\\\"b c d`, + ` a\\\\"b c" d e`, + // http://daviddeley.com/autohotkey/parameters/parameters.htm#WINARGV + // from 5.4 Examples + ` CallMeIshmael`, + ` "Call Me Ishmael"`, + ` Cal"l Me I"shmael`, + ` CallMe\"Ishmael`, + ` "CallMe\"Ishmael"`, + ` "Call Me Ishmael\\"`, + ` "CallMe\\\"Ishmael"`, + ` a\\\b`, + ` "a\\\b"`, + // from 5.5 Some Common Tasks + ` "\"Call Me Ishmael\""`, + ` "C:\TEST A\\"`, + ` "\"C:\TEST A\\\""`, + // from 5.6 The Microsoft Examples Explained + ` "a b c" d e`, + ` "ab\"c" "\\" d`, + ` a\\\b d"e f"g h`, + ` a\\\"b c d`, + ` a\\\\"b c" d e`, + // from 5.7 Double Double Quote Examples (pre 2008) + ` "a b c""`, + ` """CallMeIshmael""" b c`, + ` """Call Me Ishmael"""`, + ` """"Call Me Ishmael"" b c`, + } + for _, cmd := range cmds { + compareCommandLineToArgvWithSyscall(t, "test"+cmd) + compareCommandLineToArgvWithSyscall(t, `"cmd line"`+cmd) + compareCommandLineToArgvWithSyscall(t, exe+cmd) + + // test both syscall.EscapeArg and os.commandLineToArgv + args := os.CommandLineToArgv(exe + cmd) + out, err := testenv.Command(t, args[0], args[1:]...).CombinedOutput() + if err != nil { + t.Fatalf("running %q failed: %v\n%v", args, err, string(out)) + } + if want, have := fmt.Sprintf("%q", args), string(out); want != have { + t.Errorf("wrong output of executing %q: have %q want %q", args, have, want) + continue + } + } +} + +func findOneDriveDir() (string, error) { + // as per https://stackoverflow.com/questions/42519624/how-to-determine-location-of-onedrive-on-windows-7-and-8-in-c + const onedrivekey = `SOFTWARE\Microsoft\OneDrive` + k, err := registry.OpenKey(registry.CURRENT_USER, onedrivekey, registry.READ) + if err != nil { + return "", fmt.Errorf("OpenKey(%q) failed: %v", onedrivekey, err) + } + defer k.Close() + + path, valtype, err := k.GetStringValue("UserFolder") + if err != nil { + return "", fmt.Errorf("reading UserFolder failed: %v", err) + } + + if valtype == registry.EXPAND_SZ { + expanded, err := registry.ExpandString(path) + if err != nil { + return "", fmt.Errorf("expanding UserFolder failed: %v", err) + } + path = expanded + } + + return path, nil +} + +// TestOneDrive verifies that OneDrive folder is a directory and not a symlink. +func TestOneDrive(t *testing.T) { + t.Parallel() + + dir, err := findOneDriveDir() + if err != nil { + t.Skipf("Skipping, because we did not find OneDrive directory: %v", err) + } + testDirStats(t, dir) +} + +func TestWindowsDevNullFile(t *testing.T) { + t.Parallel() + + f1, err := os.Open("NUL") + if err != nil { + t.Fatal(err) + } + defer f1.Close() + + fi1, err := f1.Stat() + if err != nil { + t.Fatal(err) + } + + f2, err := os.Open("nul") + if err != nil { + t.Fatal(err) + } + defer f2.Close() + + fi2, err := f2.Stat() + if err != nil { + t.Fatal(err) + } + + if !os.SameFile(fi1, fi2) { + t.Errorf(`"NUL" and "nul" are not the same file`) + } +} + +func TestFileStatNUL(t *testing.T) { + t.Parallel() + + f, err := os.Open("NUL") + if err != nil { + t.Fatal(err) + } + fi, err := f.Stat() + if err != nil { + t.Fatal(err) + } + if got, want := fi.Mode(), os.ModeDevice|os.ModeCharDevice|0666; got != want { + t.Errorf("Open(%q).Stat().Mode() = %v, want %v", "NUL", got, want) + } +} + +func TestStatNUL(t *testing.T) { + t.Parallel() + + fi, err := os.Stat("NUL") + if err != nil { + t.Fatal(err) + } + if got, want := fi.Mode(), os.ModeDevice|os.ModeCharDevice|0666; got != want { + t.Errorf("Stat(%q).Mode() = %v, want %v", "NUL", got, want) + } +} + +// TestSymlinkCreation verifies that creating a symbolic link +// works on Windows when developer mode is active. +// This is supported starting Windows 10 (1703, v10.0.14972). +func TestSymlinkCreation(t *testing.T) { + if !testenv.HasSymlink() && !isWindowsDeveloperModeActive() { + t.Skip("Windows developer mode is not active") + } + t.Parallel() + + temp := t.TempDir() + dummyFile := filepath.Join(temp, "file") + if err := os.WriteFile(dummyFile, []byte(""), 0644); err != nil { + t.Fatal(err) + } + + linkFile := filepath.Join(temp, "link") + if err := os.Symlink(dummyFile, linkFile); err != nil { + t.Fatal(err) + } +} + +// isWindowsDeveloperModeActive checks whether or not the developer mode is active on Windows 10. +// Returns false for prior Windows versions. +// see https://docs.microsoft.com/en-us/windows/uwp/get-started/enable-your-device-for-development +func isWindowsDeveloperModeActive() bool { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock", registry.READ) + if err != nil { + return false + } + + val, _, err := key.GetIntegerValue("AllowDevelopmentWithoutDevLicense") + if err != nil { + return false + } + + return val != 0 +} + +// TestRootRelativeDirSymlink verifies that symlinks to paths relative to the +// drive root (beginning with "\" but no volume name) are created with the +// correct symlink type. +// (See https://golang.org/issue/39183#issuecomment-632175728.) +func TestRootRelativeDirSymlink(t *testing.T) { + testenv.MustHaveSymlink(t) + t.Parallel() + + temp := t.TempDir() + dir := filepath.Join(temp, "dir") + if err := os.Mkdir(dir, 0755); err != nil { + t.Fatal(err) + } + + volumeRelDir := strings.TrimPrefix(dir, filepath.VolumeName(dir)) // leaves leading backslash + + link := filepath.Join(temp, "link") + err := os.Symlink(volumeRelDir, link) + if err != nil { + t.Fatal(err) + } + t.Logf("Symlink(%#q, %#q)", volumeRelDir, link) + + f, err := os.Open(link) + if err != nil { + t.Fatal(err) + } + defer f.Close() + if fi, err := f.Stat(); err != nil { + t.Fatal(err) + } else if !fi.IsDir() { + t.Errorf("Open(%#q).Stat().IsDir() = false; want true", f.Name()) + } +} + +// TestWorkingDirectoryRelativeSymlink verifies that symlinks to paths relative +// to the current working directory for the drive, such as "C:File.txt", are +// correctly converted to absolute links of the correct symlink type (per +// https://docs.microsoft.com/en-us/windows/win32/fileio/creating-symbolic-links). +func TestWorkingDirectoryRelativeSymlink(t *testing.T) { + testenv.MustHaveSymlink(t) + + // Construct a directory to be symlinked. + temp := t.TempDir() + if v := filepath.VolumeName(temp); len(v) < 2 || v[1] != ':' { + t.Skipf("Can't test relative symlinks: t.TempDir() (%#q) does not begin with a drive letter.", temp) + } + + absDir := filepath.Join(temp, `dir\sub`) + if err := os.MkdirAll(absDir, 0755); err != nil { + t.Fatal(err) + } + + // Change to the temporary directory and construct a + // working-directory-relative symlink. + oldwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(oldwd); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(temp); err != nil { + t.Fatal(err) + } + t.Logf("Chdir(%#q)", temp) + + wdRelDir := filepath.VolumeName(temp) + `dir\sub` // no backslash after volume. + absLink := filepath.Join(temp, "link") + err = os.Symlink(wdRelDir, absLink) + if err != nil { + t.Fatal(err) + } + t.Logf("Symlink(%#q, %#q)", wdRelDir, absLink) + + // Now change back to the original working directory and verify that the + // symlink still refers to its original path and is correctly marked as a + // directory. + if err := os.Chdir(oldwd); err != nil { + t.Fatal(err) + } + t.Logf("Chdir(%#q)", oldwd) + + resolved, err := os.Readlink(absLink) + if err != nil { + t.Errorf("Readlink(%#q): %v", absLink, err) + } else if resolved != absDir { + t.Errorf("Readlink(%#q) = %#q; want %#q", absLink, resolved, absDir) + } + + linkFile, err := os.Open(absLink) + if err != nil { + t.Fatal(err) + } + defer linkFile.Close() + + linkInfo, err := linkFile.Stat() + if err != nil { + t.Fatal(err) + } + if !linkInfo.IsDir() { + t.Errorf("Open(%#q).Stat().IsDir() = false; want true", absLink) + } + + absInfo, err := os.Stat(absDir) + if err != nil { + t.Fatal(err) + } + + if !os.SameFile(absInfo, linkInfo) { + t.Errorf("SameFile(Stat(%#q), Open(%#q).Stat()) = false; want true", absDir, absLink) + } +} + +// TestStatOfInvalidName is regression test for issue #24999. +func TestStatOfInvalidName(t *testing.T) { + t.Parallel() + + _, err := os.Stat("*.go") + if err == nil { + t.Fatal(`os.Stat("*.go") unexpectedly succeeded`) + } +} + +// findUnusedDriveLetter searches mounted drive list on the system +// (starting from Z: and ending at D:) for unused drive letter. +// It returns path to the found drive root directory (like Z:\) or error. +func findUnusedDriveLetter() (string, error) { + // Do not use A: and B:, because they are reserved for floppy drive. + // Do not use C:, because it is normally used for main drive. + for l := 'Z'; l >= 'D'; l-- { + p := string(l) + `:\` + _, err := os.Stat(p) + if os.IsNotExist(err) { + return p, nil + } + } + return "", errors.New("Could not find unused drive letter.") +} + +func TestRootDirAsTemp(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" { + fmt.Print(os.TempDir()) + os.Exit(0) + } + + testenv.MustHaveExec(t) + t.Parallel() + + exe, err := os.Executable() + if err != nil { + t.Fatal(err) + } + + newtmp, err := findUnusedDriveLetter() + if err != nil { + t.Skip(err) + } + + cmd := testenv.Command(t, exe, "-test.run=TestRootDirAsTemp") + cmd.Env = cmd.Environ() + cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1") + cmd.Env = append(cmd.Env, "TMP="+newtmp) + cmd.Env = append(cmd.Env, "TEMP="+newtmp) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Failed to spawn child process: %v %q", err, string(output)) + } + if want, have := newtmp, string(output); have != want { + t.Fatalf("unexpected child process output %q, want %q", have, want) + } +} + +func testReadlink(t *testing.T, path, want string) { + got, err := os.Readlink(path) + if err != nil { + t.Error(err) + return + } + if got != want { + t.Errorf(`Readlink(%q): got %q, want %q`, path, got, want) + } +} + +func mklink(t *testing.T, link, target string) { + output, err := testenv.Command(t, "cmd", "/c", "mklink", link, target).CombinedOutput() + if err != nil { + t.Fatalf("failed to run mklink %v %v: %v %q", link, target, err, output) + } +} + +func mklinkj(t *testing.T, link, target string) { + output, err := testenv.Command(t, "cmd", "/c", "mklink", "/J", link, target).CombinedOutput() + if err != nil { + t.Fatalf("failed to run mklink %v %v: %v %q", link, target, err, output) + } +} + +func mklinkd(t *testing.T, link, target string) { + output, err := testenv.Command(t, "cmd", "/c", "mklink", "/D", link, target).CombinedOutput() + if err != nil { + t.Fatalf("failed to run mklink %v %v: %v %q", link, target, err, output) + } +} + +func TestWindowsReadlink(t *testing.T) { + tmpdir, err := os.MkdirTemp("", "TestWindowsReadlink") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + // Make sure tmpdir is not a symlink, otherwise tests will fail. + tmpdir, err = filepath.EvalSymlinks(tmpdir) + if err != nil { + t.Fatal(err) + } + chdir(t, tmpdir) + + vol := filepath.VolumeName(tmpdir) + output, err := testenv.Command(t, "cmd", "/c", "mountvol", vol, "/L").CombinedOutput() + if err != nil { + t.Fatalf("failed to run mountvol %v /L: %v %q", vol, err, output) + } + ntvol := strings.Trim(string(output), " \n\r") + + dir := filepath.Join(tmpdir, "dir") + err = os.MkdirAll(dir, 0777) + if err != nil { + t.Fatal(err) + } + + absdirjlink := filepath.Join(tmpdir, "absdirjlink") + mklinkj(t, absdirjlink, dir) + testReadlink(t, absdirjlink, dir) + + ntdirjlink := filepath.Join(tmpdir, "ntdirjlink") + mklinkj(t, ntdirjlink, ntvol+absdirjlink[len(filepath.VolumeName(absdirjlink)):]) + testReadlink(t, ntdirjlink, absdirjlink) + + ntdirjlinktolink := filepath.Join(tmpdir, "ntdirjlinktolink") + mklinkj(t, ntdirjlinktolink, ntvol+absdirjlink[len(filepath.VolumeName(absdirjlink)):]) + testReadlink(t, ntdirjlinktolink, absdirjlink) + + mklinkj(t, "reldirjlink", "dir") + testReadlink(t, "reldirjlink", dir) // relative directory junction resolves to absolute path + + // Make sure we have sufficient privilege to run mklink command. + testenv.MustHaveSymlink(t) + + absdirlink := filepath.Join(tmpdir, "absdirlink") + mklinkd(t, absdirlink, dir) + testReadlink(t, absdirlink, dir) + + ntdirlink := filepath.Join(tmpdir, "ntdirlink") + mklinkd(t, ntdirlink, ntvol+absdirlink[len(filepath.VolumeName(absdirlink)):]) + testReadlink(t, ntdirlink, absdirlink) + + mklinkd(t, "reldirlink", "dir") + testReadlink(t, "reldirlink", "dir") + + file := filepath.Join(tmpdir, "file") + err = os.WriteFile(file, []byte(""), 0666) + if err != nil { + t.Fatal(err) + } + + filelink := filepath.Join(tmpdir, "filelink") + mklink(t, filelink, file) + testReadlink(t, filelink, file) + + linktofilelink := filepath.Join(tmpdir, "linktofilelink") + mklink(t, linktofilelink, ntvol+filelink[len(filepath.VolumeName(filelink)):]) + testReadlink(t, linktofilelink, filelink) + + mklink(t, "relfilelink", "file") + testReadlink(t, "relfilelink", "file") +} + +func TestOpenDirTOCTOU(t *testing.T) { + t.Parallel() + + // Check opened directories can't be renamed until the handle is closed. + // See issue 52747. + tmpdir := t.TempDir() + dir := filepath.Join(tmpdir, "dir") + if err := os.Mkdir(dir, 0777); err != nil { + t.Fatal(err) + } + f, err := os.Open(dir) + if err != nil { + t.Fatal(err) + } + newpath := filepath.Join(tmpdir, "dir1") + err = os.Rename(dir, newpath) + if err == nil || !errors.Is(err, windows.ERROR_SHARING_VIOLATION) { + f.Close() + t.Fatalf("Rename(%q, %q) = %v; want windows.ERROR_SHARING_VIOLATION", dir, newpath, err) + } + f.Close() + err = os.Rename(dir, newpath) + if err != nil { + t.Error(err) + } +} + +func TestAppExecLinkStat(t *testing.T) { + // We expect executables installed to %LOCALAPPDATA%\Microsoft\WindowsApps to + // be reparse points with tag IO_REPARSE_TAG_APPEXECLINK. Here we check that + // such reparse points are treated as irregular (but executable) files, not + // broken symlinks. + appdata := os.Getenv("LOCALAPPDATA") + if appdata == "" { + t.Skipf("skipping: LOCALAPPDATA not set") + } + + pythonExeName := "python3.exe" + pythonPath := filepath.Join(appdata, `Microsoft\WindowsApps`, pythonExeName) + + lfi, err := os.Lstat(pythonPath) + if err != nil { + t.Skip("skipping test, because Python 3 is not installed via the Windows App Store on this system; see https://golang.org/issue/42919") + } + + // An APPEXECLINK reparse point is not a symlink, so os.Readlink should return + // a non-nil error for it, and Stat should return results identical to Lstat. + linkName, err := os.Readlink(pythonPath) + if err == nil { + t.Errorf("os.Readlink(%q) = %q, but expected an error\n(should be an APPEXECLINK reparse point, not a symlink)", pythonPath, linkName) + } + + sfi, err := os.Stat(pythonPath) + if err != nil { + t.Fatalf("Stat %s: %v", pythonPath, err) + } + + if lfi.Name() != sfi.Name() { + t.Logf("os.Lstat(%q) = %+v", pythonPath, lfi) + t.Logf("os.Stat(%q) = %+v", pythonPath, sfi) + t.Errorf("files should be same") + } + + if lfi.Name() != pythonExeName { + t.Errorf("Stat %s: got %q, but wanted %q", pythonPath, lfi.Name(), pythonExeName) + } + if m := lfi.Mode(); m&fs.ModeSymlink != 0 { + t.Errorf("%q should be a file, not a link (mode=0x%x)", pythonPath, uint32(m)) + } + if m := lfi.Mode(); m&fs.ModeDir != 0 { + t.Errorf("%q should be a file, not a directory (mode=0x%x)", pythonPath, uint32(m)) + } + if m := lfi.Mode(); m&fs.ModeIrregular == 0 { + // A reparse point is not a regular file, but we don't have a more appropriate + // ModeType bit for it, so it should be marked as irregular. + t.Errorf("%q should not be a regular file (mode=0x%x)", pythonPath, uint32(m)) + } + + if sfi.Name() != pythonExeName { + t.Errorf("Stat %s: got %q, but wanted %q", pythonPath, sfi.Name(), pythonExeName) + } + if m := sfi.Mode(); m&fs.ModeSymlink != 0 { + t.Errorf("%q should be a file, not a link (mode=0x%x)", pythonPath, uint32(m)) + } + if m := sfi.Mode(); m&fs.ModeDir != 0 { + t.Errorf("%q should be a file, not a directory (mode=0x%x)", pythonPath, uint32(m)) + } + if m := sfi.Mode(); m&fs.ModeIrregular == 0 { + // A reparse point is not a regular file, but we don't have a more appropriate + // ModeType bit for it, so it should be marked as irregular. + t.Errorf("%q should not be a regular file (mode=0x%x)", pythonPath, uint32(m)) + } + + p, err := exec.LookPath(pythonPath) + if err != nil { + t.Errorf("exec.LookPath(%q): %v", pythonPath, err) + } + if p != pythonPath { + t.Errorf("exec.LookPath(%q) = %q; want %q", pythonPath, p, pythonPath) + } +} + +func TestIllformedUTF16FileName(t *testing.T) { + dir := t.TempDir() + const sep = string(os.PathSeparator) + if !strings.HasSuffix(dir, sep) { + dir += sep + } + + // This UTF-16 file name is ill-formed as it contains low surrogates that are not preceded by high surrogates ([1:5]). + namew := []uint16{0x2e, 0xdc6d, 0xdc73, 0xdc79, 0xdc73, 0x30, 0x30, 0x30, 0x31, 0} + + // Create a file whose name contains unpaired surrogates. + // Use syscall.CreateFile instead of os.Create to simulate a file that is created by + // a non-Go program so the file name hasn't gone through syscall.UTF16FromString. + dirw := utf16.Encode([]rune(dir)) + pathw := append(dirw, namew...) + fd, err := syscall.CreateFile(&pathw[0], syscall.GENERIC_ALL, 0, nil, syscall.CREATE_NEW, 0, 0) + if err != nil { + t.Fatal(err) + } + syscall.CloseHandle(fd) + + name := syscall.UTF16ToString(namew) + path := filepath.Join(dir, name) + // Verify that os.Lstat can query the file. + fi, err := os.Lstat(path) + if err != nil { + t.Fatal(err) + } + if got := fi.Name(); got != name { + t.Errorf("got %q, want %q", got, name) + } + // Verify that File.Readdirnames lists the file. + f, err := os.Open(dir) + if err != nil { + t.Fatal(err) + } + files, err := f.Readdirnames(0) + f.Close() + if err != nil { + t.Fatal(err) + } + if !slices.Contains(files, name) { + t.Error("file not listed") + } + // Verify that os.RemoveAll can remove the directory + // and that it doesn't hang. + err = os.RemoveAll(dir) + if err != nil { + t.Error(err) + } +} + +func TestUTF16Alloc(t *testing.T) { + allowsPerRun := func(want int, f func()) { + t.Helper() + got := int(testing.AllocsPerRun(5, f)) + if got != want { + t.Errorf("got %d allocs, want %d", got, want) + } + } + allowsPerRun(1, func() { + syscall.UTF16ToString([]uint16{'a', 'b', 'c'}) + }) + allowsPerRun(1, func() { + syscall.UTF16FromString("abc") + }) +} + +func TestNewFileInvalid(t *testing.T) { + t.Parallel() + if f := os.NewFile(uintptr(syscall.InvalidHandle), "invalid"); f != nil { + t.Errorf("NewFile(InvalidHandle) got %v want nil", f) + } +} + +func TestReadDirPipe(t *testing.T) { + dir := `\\.\pipe\` + fi, err := os.Stat(dir) + if err != nil || !fi.IsDir() { + t.Skipf("%s is not a directory", dir) + } + _, err = os.ReadDir(dir) + if err != nil { + t.Errorf("ReadDir(%q) = %v", dir, err) + } +} diff --git a/src/os/path.go b/src/os/path.go new file mode 100644 index 0000000..df87887 --- /dev/null +++ b/src/os/path.go @@ -0,0 +1,79 @@ +// Copyright 2009 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 os + +import ( + "syscall" +) + +// MkdirAll creates a directory named path, +// along with any necessary parents, and returns nil, +// or else returns an error. +// The permission bits perm (before umask) are used for all +// directories that MkdirAll creates. +// If path is already a directory, MkdirAll does nothing +// and returns nil. +func MkdirAll(path string, perm FileMode) error { + // Fast path: if we can tell whether path is a directory or file, stop with success or error. + dir, err := Stat(path) + if err == nil { + if dir.IsDir() { + return nil + } + return &PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR} + } + + // Slow path: make sure parent exists and then call Mkdir for path. + i := len(path) + for i > 0 && IsPathSeparator(path[i-1]) { // Skip trailing path separator. + i-- + } + + j := i + for j > 0 && !IsPathSeparator(path[j-1]) { // Scan backward over element. + j-- + } + + if j > 1 { + // Create parent. + err = MkdirAll(fixRootDirectory(path[:j-1]), perm) + if err != nil { + return err + } + } + + // Parent now exists; invoke Mkdir and use its result. + err = Mkdir(path, perm) + if err != nil { + // Handle arguments like "foo/." by + // double-checking that directory doesn't exist. + dir, err1 := Lstat(path) + if err1 == nil && dir.IsDir() { + return nil + } + return err + } + return nil +} + +// RemoveAll removes path and any children it contains. +// It removes everything it can but returns the first error +// it encounters. If the path does not exist, RemoveAll +// returns nil (no error). +// If there is an error, it will be of type *PathError. +func RemoveAll(path string) error { + return removeAll(path) +} + +// endsWithDot reports whether the final component of path is ".". +func endsWithDot(path string) bool { + if path == "." { + return true + } + if len(path) >= 2 && path[len(path)-1] == '.' && IsPathSeparator(path[len(path)-2]) { + return true + } + return false +} diff --git a/src/os/path_plan9.go b/src/os/path_plan9.go new file mode 100644 index 0000000..a54b4b9 --- /dev/null +++ b/src/os/path_plan9.go @@ -0,0 +1,19 @@ +// Copyright 2011 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 os + +const ( + PathSeparator = '/' // OS-specific path separator + PathListSeparator = '\000' // OS-specific path list separator +) + +// IsPathSeparator reports whether c is a directory separator character. +func IsPathSeparator(c uint8) bool { + return PathSeparator == c +} + +func fixRootDirectory(p string) string { + return p +} diff --git a/src/os/path_test.go b/src/os/path_test.go new file mode 100644 index 0000000..2a4e956 --- /dev/null +++ b/src/os/path_test.go @@ -0,0 +1,121 @@ +// Copyright 2009 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 os_test + +import ( + "internal/testenv" + . "os" + "path/filepath" + "runtime" + "syscall" + "testing" +) + +var isReadonlyError = func(error) bool { return false } + +func TestMkdirAll(t *testing.T) { + t.Parallel() + + tmpDir := TempDir() + path := tmpDir + "/_TestMkdirAll_/dir/./dir2" + err := MkdirAll(path, 0777) + if err != nil { + t.Fatalf("MkdirAll %q: %s", path, err) + } + defer RemoveAll(tmpDir + "/_TestMkdirAll_") + + // Already exists, should succeed. + err = MkdirAll(path, 0777) + if err != nil { + t.Fatalf("MkdirAll %q (second time): %s", path, err) + } + + // Make file. + fpath := path + "/file" + f, err := Create(fpath) + if err != nil { + t.Fatalf("create %q: %s", fpath, err) + } + defer f.Close() + + // Can't make directory named after file. + err = MkdirAll(fpath, 0777) + if err == nil { + t.Fatalf("MkdirAll %q: no error", fpath) + } + perr, ok := err.(*PathError) + if !ok { + t.Fatalf("MkdirAll %q returned %T, not *PathError", fpath, err) + } + if filepath.Clean(perr.Path) != filepath.Clean(fpath) { + t.Fatalf("MkdirAll %q returned wrong error path: %q not %q", fpath, filepath.Clean(perr.Path), filepath.Clean(fpath)) + } + + // Can't make subdirectory of file. + ffpath := fpath + "/subdir" + err = MkdirAll(ffpath, 0777) + if err == nil { + t.Fatalf("MkdirAll %q: no error", ffpath) + } + perr, ok = err.(*PathError) + if !ok { + t.Fatalf("MkdirAll %q returned %T, not *PathError", ffpath, err) + } + if filepath.Clean(perr.Path) != filepath.Clean(fpath) { + t.Fatalf("MkdirAll %q returned wrong error path: %q not %q", ffpath, filepath.Clean(perr.Path), filepath.Clean(fpath)) + } + + if runtime.GOOS == "windows" { + path := tmpDir + `\_TestMkdirAll_\dir\.\dir2\` + err := MkdirAll(path, 0777) + if err != nil { + t.Fatalf("MkdirAll %q: %s", path, err) + } + } +} + +func TestMkdirAllWithSymlink(t *testing.T) { + testenv.MustHaveSymlink(t) + t.Parallel() + + tmpDir := t.TempDir() + dir := tmpDir + "/dir" + if err := Mkdir(dir, 0755); err != nil { + t.Fatalf("Mkdir %s: %s", dir, err) + } + + link := tmpDir + "/link" + if err := Symlink("dir", link); err != nil { + t.Fatalf("Symlink %s: %s", link, err) + } + + path := link + "/foo" + if err := MkdirAll(path, 0755); err != nil { + t.Errorf("MkdirAll %q: %s", path, err) + } +} + +func TestMkdirAllAtSlash(t *testing.T) { + switch runtime.GOOS { + case "android", "ios", "plan9", "windows": + t.Skipf("skipping on %s", runtime.GOOS) + } + if testenv.Builder() == "" { + t.Skipf("skipping non-hermetic test outside of Go builders") + } + + RemoveAll("/_go_os_test") + const dir = "/_go_os_test/dir" + err := MkdirAll(dir, 0777) + if err != nil { + pathErr, ok := err.(*PathError) + // common for users not to be able to write to / + if ok && (pathErr.Err == syscall.EACCES || isReadonlyError(pathErr.Err)) { + t.Skipf("could not create %v: %v", dir, err) + } + t.Fatalf(`MkdirAll "/_go_os_test/dir": %v, %s`, err, pathErr.Err) + } + RemoveAll("/_go_os_test") +} diff --git a/src/os/path_unix.go b/src/os/path_unix.go new file mode 100644 index 0000000..c975cdb --- /dev/null +++ b/src/os/path_unix.go @@ -0,0 +1,75 @@ +// Copyright 2011 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. + +//go:build unix || (js && wasm) || wasip1 + +package os + +const ( + PathSeparator = '/' // OS-specific path separator + PathListSeparator = ':' // OS-specific path list separator +) + +// IsPathSeparator reports whether c is a directory separator character. +func IsPathSeparator(c uint8) bool { + return PathSeparator == c +} + +// basename removes trailing slashes and the leading directory name from path name. +func basename(name string) string { + i := len(name) - 1 + // Remove trailing slashes + for ; i > 0 && name[i] == '/'; i-- { + name = name[:i] + } + // Remove leading directory name + for i--; i >= 0; i-- { + if name[i] == '/' { + name = name[i+1:] + break + } + } + + return name +} + +// splitPath returns the base name and parent directory. +func splitPath(path string) (string, string) { + // if no better parent is found, the path is relative from "here" + dirname := "." + + // Remove all but one leading slash. + for len(path) > 1 && path[0] == '/' && path[1] == '/' { + path = path[1:] + } + + i := len(path) - 1 + + // Remove trailing slashes. + for ; i > 0 && path[i] == '/'; i-- { + path = path[:i] + } + + // if no slashes in path, base is path + basename := path + + // Remove leading directory path + for i--; i >= 0; i-- { + if path[i] == '/' { + if i == 0 { + dirname = path[:1] + } else { + dirname = path[:i] + } + basename = path[i+1:] + break + } + } + + return dirname, basename +} + +func fixRootDirectory(p string) string { + return p +} diff --git a/src/os/path_windows.go b/src/os/path_windows.go new file mode 100644 index 0000000..3356908 --- /dev/null +++ b/src/os/path_windows.go @@ -0,0 +1,227 @@ +// Copyright 2011 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 os + +const ( + PathSeparator = '\\' // OS-specific path separator + PathListSeparator = ';' // OS-specific path list separator +) + +// IsPathSeparator reports whether c is a directory separator character. +func IsPathSeparator(c uint8) bool { + // NOTE: Windows accepts / as path separator. + return c == '\\' || c == '/' +} + +// basename removes trailing slashes and the leading +// directory name and drive letter from path name. +func basename(name string) string { + // Remove drive letter + if len(name) == 2 && name[1] == ':' { + name = "." + } else if len(name) > 2 && name[1] == ':' { + name = name[2:] + } + i := len(name) - 1 + // Remove trailing slashes + for ; i > 0 && (name[i] == '/' || name[i] == '\\'); i-- { + name = name[:i] + } + // Remove leading directory name + for i--; i >= 0; i-- { + if name[i] == '/' || name[i] == '\\' { + name = name[i+1:] + break + } + } + return name +} + +func isAbs(path string) (b bool) { + v := volumeName(path) + if v == "" { + return false + } + path = path[len(v):] + if path == "" { + return false + } + return IsPathSeparator(path[0]) +} + +func volumeName(path string) (v string) { + if len(path) < 2 { + return "" + } + // with drive letter + c := path[0] + if path[1] == ':' && + ('0' <= c && c <= '9' || 'a' <= c && c <= 'z' || + 'A' <= c && c <= 'Z') { + return path[:2] + } + // is it UNC + if l := len(path); l >= 5 && IsPathSeparator(path[0]) && IsPathSeparator(path[1]) && + !IsPathSeparator(path[2]) && path[2] != '.' { + // first, leading `\\` and next shouldn't be `\`. its server name. + for n := 3; n < l-1; n++ { + // second, next '\' shouldn't be repeated. + if IsPathSeparator(path[n]) { + n++ + // third, following something characters. its share name. + if !IsPathSeparator(path[n]) { + if path[n] == '.' { + break + } + for ; n < l; n++ { + if IsPathSeparator(path[n]) { + break + } + } + return path[:n] + } + break + } + } + } + return "" +} + +func fromSlash(path string) string { + // Replace each '/' with '\\' if present + var pathbuf []byte + var lastSlash int + for i, b := range path { + if b == '/' { + if pathbuf == nil { + pathbuf = make([]byte, len(path)) + } + copy(pathbuf[lastSlash:], path[lastSlash:i]) + pathbuf[i] = '\\' + lastSlash = i + 1 + } + } + if pathbuf == nil { + return path + } + + copy(pathbuf[lastSlash:], path[lastSlash:]) + return string(pathbuf) +} + +func dirname(path string) string { + vol := volumeName(path) + i := len(path) - 1 + for i >= len(vol) && !IsPathSeparator(path[i]) { + i-- + } + dir := path[len(vol) : i+1] + last := len(dir) - 1 + if last > 0 && IsPathSeparator(dir[last]) { + dir = dir[:last] + } + if dir == "" { + dir = "." + } + return vol + dir +} + +// This is set via go:linkname on runtime.canUseLongPaths, and is true when the OS +// supports opting into proper long path handling without the need for fixups. +var canUseLongPaths bool + +// fixLongPath returns the extended-length (\\?\-prefixed) form of +// path when needed, in order to avoid the default 260 character file +// path limit imposed by Windows. If path is not easily converted to +// the extended-length form (for example, if path is a relative path +// or contains .. elements), or is short enough, fixLongPath returns +// path unmodified. +// +// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath +func fixLongPath(path string) string { + if canUseLongPaths { + return path + } + // Do nothing (and don't allocate) if the path is "short". + // Empirically (at least on the Windows Server 2013 builder), + // the kernel is arbitrarily okay with < 248 bytes. That + // matches what the docs above say: + // "When using an API to create a directory, the specified + // path cannot be so long that you cannot append an 8.3 file + // name (that is, the directory name cannot exceed MAX_PATH + // minus 12)." Since MAX_PATH is 260, 260 - 12 = 248. + // + // The MSDN docs appear to say that a normal path that is 248 bytes long + // will work; empirically the path must be less then 248 bytes long. + if len(path) < 248 { + // Don't fix. (This is how Go 1.7 and earlier worked, + // not automatically generating the \\?\ form) + return path + } + + // The extended form begins with \\?\, as in + // \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt. + // The extended form disables evaluation of . and .. path + // elements and disables the interpretation of / as equivalent + // to \. The conversion here rewrites / to \ and elides + // . elements as well as trailing or duplicate separators. For + // simplicity it avoids the conversion entirely for relative + // paths or paths containing .. elements. For now, + // \\server\share paths are not converted to + // \\?\UNC\server\share paths because the rules for doing so + // are less well-specified. + if len(path) >= 2 && path[:2] == `\\` { + // Don't canonicalize UNC paths. + return path + } + if !isAbs(path) { + // Relative path + return path + } + + const prefix = `\\?` + + pathbuf := make([]byte, len(prefix)+len(path)+len(`\`)) + copy(pathbuf, prefix) + n := len(path) + r, w := 0, len(prefix) + for r < n { + switch { + case IsPathSeparator(path[r]): + // empty block + r++ + case path[r] == '.' && (r+1 == n || IsPathSeparator(path[r+1])): + // /./ + r++ + case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || IsPathSeparator(path[r+2])): + // /../ is currently unhandled + return path + default: + pathbuf[w] = '\\' + w++ + for ; r < n && !IsPathSeparator(path[r]); r++ { + pathbuf[w] = path[r] + w++ + } + } + } + // A drive's root directory needs a trailing \ + if w == len(`\\?\c:`) { + pathbuf[w] = '\\' + w++ + } + return string(pathbuf[:w]) +} + +// fixRootDirectory fixes a reference to a drive's root directory to +// have the required trailing slash. +func fixRootDirectory(p string) string { + if len(p) == len(`\\?\c:`) { + if IsPathSeparator(p[0]) && IsPathSeparator(p[1]) && p[2] == '?' && IsPathSeparator(p[3]) && p[5] == ':' { + return p + `\` + } + } + return p +} diff --git a/src/os/path_windows_test.go b/src/os/path_windows_test.go new file mode 100644 index 0000000..2506b4f --- /dev/null +++ b/src/os/path_windows_test.go @@ -0,0 +1,108 @@ +// Copyright 2016 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 os_test + +import ( + "os" + "strings" + "syscall" + "testing" +) + +func TestFixLongPath(t *testing.T) { + if os.CanUseLongPaths { + return + } + t.Parallel() + + // 248 is long enough to trigger the longer-than-248 checks in + // fixLongPath, but short enough not to make a path component + // longer than 255, which is illegal on Windows. (which + // doesn't really matter anyway, since this is purely a string + // function we're testing, and it's not actually being used to + // do a system call) + veryLong := "l" + strings.Repeat("o", 248) + "ng" + for _, test := range []struct{ in, want string }{ + // Short; unchanged: + {`C:\short.txt`, `C:\short.txt`}, + {`C:\`, `C:\`}, + {`C:`, `C:`}, + // The "long" substring is replaced by a looooooong + // string which triggers the rewriting. Except in the + // cases below where it doesn't. + {`C:\long\foo.txt`, `\\?\C:\long\foo.txt`}, + {`C:/long/foo.txt`, `\\?\C:\long\foo.txt`}, + {`C:\long\foo\\bar\.\baz\\`, `\\?\C:\long\foo\bar\baz`}, + {`\\unc\path`, `\\unc\path`}, + {`long.txt`, `long.txt`}, + {`C:long.txt`, `C:long.txt`}, + {`c:\long\..\bar\baz`, `c:\long\..\bar\baz`}, + {`\\?\c:\long\foo.txt`, `\\?\c:\long\foo.txt`}, + {`\\?\c:\long/foo.txt`, `\\?\c:\long/foo.txt`}, + } { + in := strings.ReplaceAll(test.in, "long", veryLong) + want := strings.ReplaceAll(test.want, "long", veryLong) + if got := os.FixLongPath(in); got != want { + got = strings.ReplaceAll(got, veryLong, "long") + t.Errorf("fixLongPath(%q) = %q; want %q", test.in, got, test.want) + } + } +} + +func TestMkdirAllLongPath(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + path := tmpDir + for i := 0; i < 100; i++ { + path += `\another-path-component` + } + if err := os.MkdirAll(path, 0777); err != nil { + t.Fatalf("MkdirAll(%q) failed; %v", path, err) + } + if err := os.RemoveAll(tmpDir); err != nil { + t.Fatalf("RemoveAll(%q) failed; %v", tmpDir, err) + } +} + +func TestMkdirAllExtendedLength(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + + const prefix = `\\?\` + if len(tmpDir) < 4 || tmpDir[:4] != prefix { + fullPath, err := syscall.FullPath(tmpDir) + if err != nil { + t.Fatalf("FullPath(%q) fails: %v", tmpDir, err) + } + tmpDir = prefix + fullPath + } + path := tmpDir + `\dir\` + if err := os.MkdirAll(path, 0777); err != nil { + t.Fatalf("MkdirAll(%q) failed: %v", path, err) + } + + path = path + `.\dir2` + if err := os.MkdirAll(path, 0777); err == nil { + t.Fatalf("MkdirAll(%q) should have failed, but did not", path) + } +} + +func TestOpenRootSlash(t *testing.T) { + t.Parallel() + + tests := []string{ + `/`, + `\`, + } + + for _, test := range tests { + dir, err := os.Open(test) + if err != nil { + t.Fatalf("Open(%q) failed: %v", test, err) + } + dir.Close() + } +} diff --git a/src/os/pipe2_unix.go b/src/os/pipe2_unix.go new file mode 100644 index 0000000..2d293fd --- /dev/null +++ b/src/os/pipe2_unix.go @@ -0,0 +1,22 @@ +// Copyright 2017 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. + +//go:build dragonfly || freebsd || linux || netbsd || openbsd || solaris + +package os + +import "syscall" + +// Pipe returns a connected pair of Files; reads from r return bytes written to w. +// It returns the files and an error, if any. +func Pipe() (r *File, w *File, err error) { + var p [2]int + + e := syscall.Pipe2(p[0:], syscall.O_CLOEXEC) + if e != nil { + return nil, nil, NewSyscallError("pipe2", e) + } + + return newFile(p[0], "|0", kindPipe), newFile(p[1], "|1", kindPipe), nil +} diff --git a/src/os/pipe_test.go b/src/os/pipe_test.go new file mode 100644 index 0000000..6f01d30 --- /dev/null +++ b/src/os/pipe_test.go @@ -0,0 +1,478 @@ +// Copyright 2015 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. + +// Test broken pipes on Unix systems. +// +//go:build !plan9 && !js && !wasip1 + +package os_test + +import ( + "bufio" + "bytes" + "fmt" + "internal/testenv" + "io" + "io/fs" + "os" + "os/exec" + "os/signal" + "runtime" + "strconv" + "strings" + "sync" + "syscall" + "testing" + "time" +) + +func TestEPIPE(t *testing.T) { + // This test cannot be run in parallel because of a race similar + // to the one reported in https://go.dev/issue/22315. + // + // Even though the pipe is opened with O_CLOEXEC, if another test forks in + // between the call to os.Pipe and the call to r.Close, that child process can + // retain an open copy of r's file descriptor until it execs. If one of our + // Write calls occurs during that interval it can spuriously succeed, + // buffering the write to the child's copy of the pipe (even though the child + // will not actually read the buffered bytes). + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + if err := r.Close(); err != nil { + t.Fatal(err) + } + + expect := syscall.EPIPE + if runtime.GOOS == "windows" { + // 232 is Windows error code ERROR_NO_DATA, "The pipe is being closed". + expect = syscall.Errno(232) + } + // Every time we write to the pipe we should get an EPIPE. + for i := 0; i < 20; i++ { + _, err = w.Write([]byte("hi")) + if err == nil { + t.Fatal("unexpected success of Write to broken pipe") + } + if pe, ok := err.(*fs.PathError); ok { + err = pe.Err + } + if se, ok := err.(*os.SyscallError); ok { + err = se.Err + } + if err != expect { + t.Errorf("iteration %d: got %v, expected %v", i, err, expect) + } + } +} + +func TestStdPipe(t *testing.T) { + switch runtime.GOOS { + case "windows": + t.Skip("Windows doesn't support SIGPIPE") + } + + if os.Getenv("GO_TEST_STD_PIPE_HELPER") != "" { + if os.Getenv("GO_TEST_STD_PIPE_HELPER_SIGNAL") != "" { + signal.Notify(make(chan os.Signal, 1), syscall.SIGPIPE) + } + switch os.Getenv("GO_TEST_STD_PIPE_HELPER") { + case "1": + os.Stdout.Write([]byte("stdout")) + case "2": + os.Stderr.Write([]byte("stderr")) + case "3": + if _, err := os.NewFile(3, "3").Write([]byte("3")); err == nil { + os.Exit(3) + } + default: + panic("unrecognized value for GO_TEST_STD_PIPE_HELPER") + } + // For stdout/stderr, we should have crashed with a broken pipe error. + // The caller will be looking for that exit status, + // so just exit normally here to cause a failure in the caller. + // For descriptor 3, a normal exit is expected. + os.Exit(0) + } + + testenv.MustHaveExec(t) + // This test cannot be run in parallel due to the same race as for TestEPIPE. + // (We expect a write to a closed pipe can fail, but a concurrent fork of a + // child process can cause the pipe to unexpectedly remain open.) + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + if err := r.Close(); err != nil { + t.Fatal(err) + } + // Invoke the test program to run the test and write to a closed pipe. + // If sig is false: + // writing to stdout or stderr should cause an immediate SIGPIPE; + // writing to descriptor 3 should fail with EPIPE and then exit 0. + // If sig is true: + // all writes should fail with EPIPE and then exit 0. + for _, sig := range []bool{false, true} { + for dest := 1; dest < 4; dest++ { + cmd := testenv.Command(t, os.Args[0], "-test.run", "TestStdPipe") + cmd.Stdout = w + cmd.Stderr = w + cmd.ExtraFiles = []*os.File{w} + cmd.Env = append(os.Environ(), fmt.Sprintf("GO_TEST_STD_PIPE_HELPER=%d", dest)) + if sig { + cmd.Env = append(cmd.Env, "GO_TEST_STD_PIPE_HELPER_SIGNAL=1") + } + if err := cmd.Run(); err == nil { + if !sig && dest < 3 { + t.Errorf("unexpected success of write to closed pipe %d sig %t in child", dest, sig) + } + } else if ee, ok := err.(*exec.ExitError); !ok { + t.Errorf("unexpected exec error type %T: %v", err, err) + } else if ws, ok := ee.Sys().(syscall.WaitStatus); !ok { + t.Errorf("unexpected wait status type %T: %v", ee.Sys(), ee.Sys()) + } else if ws.Signaled() && ws.Signal() == syscall.SIGPIPE { + if sig || dest > 2 { + t.Errorf("unexpected SIGPIPE signal for descriptor %d sig %t", dest, sig) + } + } else { + t.Errorf("unexpected exit status %v for descriptor %d sig %t", err, dest, sig) + } + } + } + + // Test redirecting stdout but not stderr. Issue 40076. + cmd := testenv.Command(t, os.Args[0], "-test.run", "TestStdPipe") + cmd.Stdout = w + var stderr bytes.Buffer + cmd.Stderr = &stderr + cmd.Env = append(cmd.Environ(), "GO_TEST_STD_PIPE_HELPER=1") + if err := cmd.Run(); err == nil { + t.Errorf("unexpected success of write to closed stdout") + } else if ee, ok := err.(*exec.ExitError); !ok { + t.Errorf("unexpected exec error type %T: %v", err, err) + } else if ws, ok := ee.Sys().(syscall.WaitStatus); !ok { + t.Errorf("unexpected wait status type %T: %v", ee.Sys(), ee.Sys()) + } else if !ws.Signaled() || ws.Signal() != syscall.SIGPIPE { + t.Errorf("unexpected exit status %v for write to closed stdout", err) + } + if output := stderr.Bytes(); len(output) > 0 { + t.Errorf("unexpected output on stderr: %s", output) + } +} + +func testClosedPipeRace(t *testing.T, read bool) { + // This test cannot be run in parallel due to the same race as for TestEPIPE. + // (We expect a write to a closed pipe can fail, but a concurrent fork of a + // child process can cause the pipe to unexpectedly remain open.) + + limit := 1 + if !read { + // Get the amount we have to write to overload a pipe + // with no reader. + limit = 131073 + if b, err := os.ReadFile("/proc/sys/fs/pipe-max-size"); err == nil { + if i, err := strconv.Atoi(strings.TrimSpace(string(b))); err == nil { + limit = i + 1 + } + } + t.Logf("using pipe write limit of %d", limit) + } + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + // Close the read end of the pipe in a goroutine while we are + // writing to the write end, or vice-versa. + go func() { + // Give the main goroutine a chance to enter the Read or + // Write call. This is sloppy but the test will pass even + // if we close before the read/write. + time.Sleep(20 * time.Millisecond) + + var err error + if read { + err = r.Close() + } else { + err = w.Close() + } + if err != nil { + t.Error(err) + } + }() + + b := make([]byte, limit) + if read { + _, err = r.Read(b[:]) + } else { + _, err = w.Write(b[:]) + } + if err == nil { + t.Error("I/O on closed pipe unexpectedly succeeded") + } else if pe, ok := err.(*fs.PathError); !ok { + t.Errorf("I/O on closed pipe returned unexpected error type %T; expected fs.PathError", pe) + } else if pe.Err != fs.ErrClosed { + t.Errorf("got error %q but expected %q", pe.Err, fs.ErrClosed) + } else { + t.Logf("I/O returned expected error %q", err) + } +} + +func TestClosedPipeRaceRead(t *testing.T) { + testClosedPipeRace(t, true) +} + +func TestClosedPipeRaceWrite(t *testing.T) { + testClosedPipeRace(t, false) +} + +// Issue 20915: Reading on nonblocking fd should not return "waiting +// for unsupported file type." Currently it returns EAGAIN; it is +// possible that in the future it will simply wait for data. +func TestReadNonblockingFd(t *testing.T) { + switch runtime.GOOS { + case "windows": + t.Skip("Windows doesn't support SetNonblock") + } + if os.Getenv("GO_WANT_READ_NONBLOCKING_FD") == "1" { + fd := syscallDescriptor(os.Stdin.Fd()) + syscall.SetNonblock(fd, true) + defer syscall.SetNonblock(fd, false) + _, err := os.Stdin.Read(make([]byte, 1)) + if err != nil { + if perr, ok := err.(*fs.PathError); !ok || perr.Err != syscall.EAGAIN { + t.Fatalf("read on nonblocking stdin got %q, should have gotten EAGAIN", err) + } + } + os.Exit(0) + } + + testenv.MustHaveExec(t) + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + cmd := testenv.Command(t, os.Args[0], "-test.run="+t.Name()) + cmd.Env = append(cmd.Environ(), "GO_WANT_READ_NONBLOCKING_FD=1") + cmd.Stdin = r + output, err := cmd.CombinedOutput() + t.Logf("%s", output) + if err != nil { + t.Errorf("child process failed: %v", err) + } +} + +func TestCloseWithBlockingReadByNewFile(t *testing.T) { + t.Parallel() + + var p [2]syscallDescriptor + err := syscall.Pipe(p[:]) + if err != nil { + t.Fatal(err) + } + // os.NewFile returns a blocking mode file. + testCloseWithBlockingRead(t, os.NewFile(uintptr(p[0]), "reader"), os.NewFile(uintptr(p[1]), "writer")) +} + +func TestCloseWithBlockingReadByFd(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + // Calling Fd will put the file into blocking mode. + _ = r.Fd() + testCloseWithBlockingRead(t, r, w) +} + +// Test that we don't let a blocking read prevent a close. +func testCloseWithBlockingRead(t *testing.T, r, w *os.File) { + var ( + enteringRead = make(chan struct{}) + done = make(chan struct{}) + ) + go func() { + var b [1]byte + close(enteringRead) + _, err := r.Read(b[:]) + if err == nil { + t.Error("I/O on closed pipe unexpectedly succeeded") + } + + if pe, ok := err.(*fs.PathError); ok { + err = pe.Err + } + if err != io.EOF && err != fs.ErrClosed { + t.Errorf("got %v, expected EOF or closed", err) + } + close(done) + }() + + // Give the goroutine a chance to enter the Read + // or Write call. This is sloppy but the test will + // pass even if we close before the read/write. + <-enteringRead + time.Sleep(20 * time.Millisecond) + + if err := r.Close(); err != nil { + t.Error(err) + } + // r.Close has completed, but since we assume r is in blocking mode that + // probably didn't unblock the call to r.Read. Close w to unblock it. + w.Close() + <-done +} + +func TestPipeEOF(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + + testPipeEOF(t, r, w) +} + +// testPipeEOF tests that when the write side of a pipe or FIFO is closed, +// a blocked Read call on the reader side returns io.EOF. +// +// This scenario previously failed to unblock the Read call on darwin. +// (See https://go.dev/issue/24164.) +func testPipeEOF(t *testing.T, r io.ReadCloser, w io.WriteCloser) { + // parkDelay is an arbitrary delay we wait for a pipe-reader goroutine to park + // before issuing the corresponding write. The test should pass no matter what + // delay we use, but with a longer delay is has a higher chance of detecting + // poller bugs. + parkDelay := 10 * time.Millisecond + if testing.Short() { + parkDelay = 100 * time.Microsecond + } + writerDone := make(chan struct{}) + defer func() { + if err := r.Close(); err != nil { + t.Errorf("error closing reader: %v", err) + } + <-writerDone + }() + + write := make(chan int, 1) + go func() { + defer close(writerDone) + + for i := range write { + time.Sleep(parkDelay) + _, err := fmt.Fprintf(w, "line %d\n", i) + if err != nil { + t.Errorf("error writing to fifo: %v", err) + return + } + } + + time.Sleep(parkDelay) + if err := w.Close(); err != nil { + t.Errorf("error closing writer: %v", err) + } + }() + + rbuf := bufio.NewReader(r) + for i := 0; i < 3; i++ { + write <- i + b, err := rbuf.ReadBytes('\n') + if err != nil { + t.Fatal(err) + } + t.Logf("%s\n", bytes.TrimSpace(b)) + } + + close(write) + b, err := rbuf.ReadBytes('\n') + if err != io.EOF || len(b) != 0 { + t.Errorf(`ReadBytes: %q, %v; want "", io.EOF`, b, err) + } +} + +// Issue 24481. +func TestFdRace(t *testing.T) { + // This test starts 100 simultaneous goroutines, which could bury a more + // interesting stack if this or some other test happens to panic. It is also + // nearly instantaneous, so any latency benefit from running it in parallel + // would be minimal. + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + var wg sync.WaitGroup + call := func() { + defer wg.Done() + w.Fd() + } + + const tries = 100 + for i := 0; i < tries; i++ { + wg.Add(1) + go call() + } + wg.Wait() +} + +func TestFdReadRace(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + const count = 10 + + c := make(chan bool, 1) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + var buf [count]byte + r.SetReadDeadline(time.Now().Add(time.Minute)) + c <- true + if _, err := r.Read(buf[:]); os.IsTimeout(err) { + t.Error("read timed out") + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + <-c + // Give the other goroutine a chance to enter the Read. + // It doesn't matter if this occasionally fails, the test + // will still pass, it just won't test anything. + time.Sleep(10 * time.Millisecond) + r.Fd() + + // The bug was that Fd would hang until Read timed out. + // If the bug is fixed, then writing to w and closing r here + // will cause the Read to exit before the timeout expires. + w.Write(make([]byte, count)) + r.Close() + }() + + wg.Wait() +} diff --git a/src/os/pipe_unix.go b/src/os/pipe_unix.go new file mode 100644 index 0000000..2eb11a0 --- /dev/null +++ b/src/os/pipe_unix.go @@ -0,0 +1,28 @@ +// Copyright 2009 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. + +//go:build aix || darwin + +package os + +import "syscall" + +// Pipe returns a connected pair of Files; reads from r return bytes written to w. +// It returns the files and an error, if any. +func Pipe() (r *File, w *File, err error) { + var p [2]int + + // See ../syscall/exec.go for description of lock. + syscall.ForkLock.RLock() + e := syscall.Pipe(p[0:]) + if e != nil { + syscall.ForkLock.RUnlock() + return nil, nil, NewSyscallError("pipe", e) + } + syscall.CloseOnExec(p[0]) + syscall.CloseOnExec(p[1]) + syscall.ForkLock.RUnlock() + + return newFile(p[0], "|0", kindPipe), newFile(p[1], "|1", kindPipe), nil +} diff --git a/src/os/pipe_wasm.go b/src/os/pipe_wasm.go new file mode 100644 index 0000000..87a29b1 --- /dev/null +++ b/src/os/pipe_wasm.go @@ -0,0 +1,16 @@ +// 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. + +//go:build wasm + +package os + +import "syscall" + +// Pipe returns a connected pair of Files; reads from r return bytes written to w. +// It returns the files and an error, if any. +func Pipe() (r *File, w *File, err error) { + // Neither GOOS=js nor GOOS=wasip1 have pipes. + return nil, nil, NewSyscallError("pipe", syscall.ENOSYS) +} diff --git a/src/os/proc.go b/src/os/proc.go new file mode 100644 index 0000000..3aae568 --- /dev/null +++ b/src/os/proc.go @@ -0,0 +1,80 @@ +// Copyright 2009 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. + +// Process etc. + +package os + +import ( + "internal/testlog" + "runtime" + "syscall" +) + +// Args hold the command-line arguments, starting with the program name. +var Args []string + +func init() { + if runtime.GOOS == "windows" { + // Initialized in exec_windows.go. + return + } + Args = runtime_args() +} + +func runtime_args() []string // in package runtime + +// Getuid returns the numeric user id of the caller. +// +// On Windows, it returns -1. +func Getuid() int { return syscall.Getuid() } + +// Geteuid returns the numeric effective user id of the caller. +// +// On Windows, it returns -1. +func Geteuid() int { return syscall.Geteuid() } + +// Getgid returns the numeric group id of the caller. +// +// On Windows, it returns -1. +func Getgid() int { return syscall.Getgid() } + +// Getegid returns the numeric effective group id of the caller. +// +// On Windows, it returns -1. +func Getegid() int { return syscall.Getegid() } + +// Getgroups returns a list of the numeric ids of groups that the caller belongs to. +// +// On Windows, it returns syscall.EWINDOWS. See the os/user package +// for a possible alternative. +func Getgroups() ([]int, error) { + gids, e := syscall.Getgroups() + return gids, NewSyscallError("getgroups", e) +} + +// Exit causes the current program to exit with the given status code. +// Conventionally, code zero indicates success, non-zero an error. +// The program terminates immediately; deferred functions are not run. +// +// For portability, the status code should be in the range [0, 125]. +func Exit(code int) { + if code == 0 && testlog.PanicOnExit0() { + // We were told to panic on calls to os.Exit(0). + // This is used to fail tests that make an early + // unexpected call to os.Exit(0). + panic("unexpected call to os.Exit(0) during test") + } + + // Inform the runtime that os.Exit is being called. If -race is + // enabled, this will give race detector a chance to fail the + // program (racy programs do not have the right to finish + // successfully). If coverage is enabled, then this call will + // enable us to write out a coverage data file. + runtime_beforeExit(code) + + syscall.Exit(code) +} + +func runtime_beforeExit(exitCode int) // implemented in runtime diff --git a/src/os/rawconn.go b/src/os/rawconn.go new file mode 100644 index 0000000..14a495d --- /dev/null +++ b/src/os/rawconn.go @@ -0,0 +1,47 @@ +// Copyright 2018 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. + +//go:build !plan9 + +package os + +import ( + "runtime" +) + +// rawConn implements syscall.RawConn. +type rawConn struct { + file *File +} + +func (c *rawConn) Control(f func(uintptr)) error { + if err := c.file.checkValid("SyscallConn.Control"); err != nil { + return err + } + err := c.file.pfd.RawControl(f) + runtime.KeepAlive(c.file) + return err +} + +func (c *rawConn) Read(f func(uintptr) bool) error { + if err := c.file.checkValid("SyscallConn.Read"); err != nil { + return err + } + err := c.file.pfd.RawRead(f) + runtime.KeepAlive(c.file) + return err +} + +func (c *rawConn) Write(f func(uintptr) bool) error { + if err := c.file.checkValid("SyscallConn.Write"); err != nil { + return err + } + err := c.file.pfd.RawWrite(f) + runtime.KeepAlive(c.file) + return err +} + +func newRawConn(file *File) (*rawConn, error) { + return &rawConn{file: file}, nil +} diff --git a/src/os/rawconn_test.go b/src/os/rawconn_test.go new file mode 100644 index 0000000..8aae7ae --- /dev/null +++ b/src/os/rawconn_test.go @@ -0,0 +1,66 @@ +// Copyright 2018 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. + +// Test use of raw connections. +// +//go:build !plan9 && !js && !wasip1 + +package os_test + +import ( + "os" + "syscall" + "testing" +) + +func TestRawConnReadWrite(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + rconn, err := r.SyscallConn() + if err != nil { + t.Fatal(err) + } + wconn, err := w.SyscallConn() + if err != nil { + t.Fatal(err) + } + + var operr error + err = wconn.Write(func(s uintptr) bool { + _, operr = syscall.Write(syscallDescriptor(s), []byte{'b'}) + return operr != syscall.EAGAIN + }) + if err != nil { + t.Fatal(err) + } + if operr != nil { + t.Fatal(err) + } + + var n int + buf := make([]byte, 1) + err = rconn.Read(func(s uintptr) bool { + n, operr = syscall.Read(syscallDescriptor(s), buf) + return operr != syscall.EAGAIN + }) + if err != nil { + t.Fatal(err) + } + if operr != nil { + t.Fatal(operr) + } + if n != 1 { + t.Errorf("read %d bytes, expected 1", n) + } + if buf[0] != 'b' { + t.Errorf("read %q, expected %q", buf, "b") + } +} diff --git a/src/os/read_test.go b/src/os/read_test.go new file mode 100644 index 0000000..18f7d54 --- /dev/null +++ b/src/os/read_test.go @@ -0,0 +1,138 @@ +// Copyright 2009 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 os_test + +import ( + "bytes" + . "os" + "path/filepath" + "runtime" + "testing" +) + +func checkNamedSize(t *testing.T, path string, size int64) { + dir, err := Stat(path) + if err != nil { + t.Fatalf("Stat %q (looking for size %d): %s", path, size, err) + } + if dir.Size() != size { + t.Errorf("Stat %q: size %d want %d", path, dir.Size(), size) + } +} + +func TestReadFile(t *testing.T) { + t.Parallel() + + filename := "rumpelstilzchen" + contents, err := ReadFile(filename) + if err == nil { + t.Fatalf("ReadFile %s: error expected, none found", filename) + } + + filename = "read_test.go" + contents, err = ReadFile(filename) + if err != nil { + t.Fatalf("ReadFile %s: %v", filename, err) + } + + checkNamedSize(t, filename, int64(len(contents))) +} + +func TestWriteFile(t *testing.T) { + t.Parallel() + + f, err := CreateTemp("", "ioutil-test") + if err != nil { + t.Fatal(err) + } + defer f.Close() + defer Remove(f.Name()) + + msg := "Programming today is a race between software engineers striving to " + + "build bigger and better idiot-proof programs, and the Universe trying " + + "to produce bigger and better idiots. So far, the Universe is winning." + + if err := WriteFile(f.Name(), []byte(msg), 0644); err != nil { + t.Fatalf("WriteFile %s: %v", f.Name(), err) + } + + data, err := ReadFile(f.Name()) + if err != nil { + t.Fatalf("ReadFile %s: %v", f.Name(), err) + } + + if string(data) != msg { + t.Fatalf("ReadFile: wrong data:\nhave %q\nwant %q", string(data), msg) + } +} + +func TestReadOnlyWriteFile(t *testing.T) { + if Getuid() == 0 { + t.Skipf("Root can write to read-only files anyway, so skip the read-only test.") + } + if runtime.GOOS == "wasip1" { + t.Skip("no support for file permissions on " + runtime.GOOS) + } + t.Parallel() + + // We don't want to use CreateTemp directly, since that opens a file for us as 0600. + tempDir, err := MkdirTemp("", t.Name()) + if err != nil { + t.Fatal(err) + } + defer RemoveAll(tempDir) + filename := filepath.Join(tempDir, "blurp.txt") + + shmorp := []byte("shmorp") + florp := []byte("florp") + err = WriteFile(filename, shmorp, 0444) + if err != nil { + t.Fatalf("WriteFile %s: %v", filename, err) + } + err = WriteFile(filename, florp, 0444) + if err == nil { + t.Fatalf("Expected an error when writing to read-only file %s", filename) + } + got, err := ReadFile(filename) + if err != nil { + t.Fatalf("ReadFile %s: %v", filename, err) + } + if !bytes.Equal(got, shmorp) { + t.Fatalf("want %s, got %s", shmorp, got) + } +} + +func TestReadDir(t *testing.T) { + t.Parallel() + + dirname := "rumpelstilzchen" + _, err := ReadDir(dirname) + if err == nil { + t.Fatalf("ReadDir %s: error expected, none found", dirname) + } + + dirname = "." + list, err := ReadDir(dirname) + if err != nil { + t.Fatalf("ReadDir %s: %v", dirname, err) + } + + foundFile := false + foundSubDir := false + for _, dir := range list { + switch { + case !dir.IsDir() && dir.Name() == "read_test.go": + foundFile = true + case dir.IsDir() && dir.Name() == "exec": + foundSubDir = true + } + } + if !foundFile { + t.Fatalf("ReadDir %s: read_test.go file not found", dirname) + } + if !foundSubDir { + t.Fatalf("ReadDir %s: exec directory not found", dirname) + } +} diff --git a/src/os/readfrom_linux.go b/src/os/readfrom_linux.go new file mode 100644 index 0000000..7e80240 --- /dev/null +++ b/src/os/readfrom_linux.go @@ -0,0 +1,124 @@ +// 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 os + +import ( + "internal/poll" + "io" + "syscall" +) + +var ( + pollCopyFileRange = poll.CopyFileRange + pollSplice = poll.Splice +) + +func (f *File) readFrom(r io.Reader) (written int64, handled bool, err error) { + // Neither copy_file_range(2) nor splice(2) supports destinations opened with + // O_APPEND, so don't bother to try zero-copy with these system calls. + // + // Visit https://man7.org/linux/man-pages/man2/copy_file_range.2.html#ERRORS and + // https://man7.org/linux/man-pages/man2/splice.2.html#ERRORS for details. + if f.appendMode { + return 0, false, nil + } + + written, handled, err = f.copyFileRange(r) + if handled { + return + } + return f.spliceToFile(r) +} + +func (f *File) spliceToFile(r io.Reader) (written int64, handled bool, err error) { + var ( + remain int64 + lr *io.LimitedReader + ) + if lr, r, remain = tryLimitedReader(r); remain <= 0 { + return 0, true, nil + } + + pfd := getPollFD(r) + // TODO(panjf2000): run some tests to see if we should unlock the non-streams for splice. + // Streams benefit the most from the splice(2), non-streams are not even supported in old kernels + // where splice(2) will just return EINVAL; newer kernels support non-streams like UDP, but I really + // doubt that splice(2) could help non-streams, cuz they usually send small frames respectively + // and one splice call would result in one frame. + // splice(2) is suitable for large data but the generation of fragments defeats its edge here. + // Therefore, don't bother to try splice if the r is not a streaming descriptor. + if pfd == nil || !pfd.IsStream { + return + } + + var syscallName string + written, handled, syscallName, err = pollSplice(&f.pfd, pfd, remain) + + if lr != nil { + lr.N = remain - written + } + + return written, handled, wrapSyscallError(syscallName, err) +} + +// getPollFD tries to get the poll.FD from the given io.Reader by expecting +// the underlying type of r to be the implementation of syscall.Conn that contains +// a *net.rawConn. +func getPollFD(r io.Reader) *poll.FD { + sc, ok := r.(syscall.Conn) + if !ok { + return nil + } + rc, err := sc.SyscallConn() + if err != nil { + return nil + } + ipfd, ok := rc.(interface{ PollFD() *poll.FD }) + if !ok { + return nil + } + return ipfd.PollFD() +} + +func (f *File) copyFileRange(r io.Reader) (written int64, handled bool, err error) { + var ( + remain int64 + lr *io.LimitedReader + ) + if lr, r, remain = tryLimitedReader(r); remain <= 0 { + return 0, true, nil + } + + src, ok := r.(*File) + if !ok { + return 0, false, nil + } + if src.checkValid("ReadFrom") != nil { + // Avoid returning the error as we report handled as false, + // leave further error handling as the responsibility of the caller. + return 0, false, nil + } + + written, handled, err = pollCopyFileRange(&f.pfd, &src.pfd, remain) + if lr != nil { + lr.N -= written + } + return written, handled, wrapSyscallError("copy_file_range", err) +} + +// tryLimitedReader tries to assert the io.Reader to io.LimitedReader, it returns the io.LimitedReader, +// the underlying io.Reader and the remaining amount of bytes if the assertion succeeds, +// otherwise it just returns the original io.Reader and the theoretical unlimited remaining amount of bytes. +func tryLimitedReader(r io.Reader) (*io.LimitedReader, io.Reader, int64) { + var remain int64 = 1<<63 - 1 // by default, copy until EOF + + lr, ok := r.(*io.LimitedReader) + if !ok { + return nil, r, remain + } + + remain = lr.N + return lr, lr.R, remain +} diff --git a/src/os/readfrom_linux_test.go b/src/os/readfrom_linux_test.go new file mode 100644 index 0000000..4f98be4 --- /dev/null +++ b/src/os/readfrom_linux_test.go @@ -0,0 +1,822 @@ +// 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 os_test + +import ( + "bytes" + "errors" + "internal/poll" + "internal/testpty" + "io" + "math/rand" + "net" + . "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" + "syscall" + "testing" + "time" + + "golang.org/x/net/nettest" +) + +func TestCopyFileRange(t *testing.T) { + sizes := []int{ + 1, + 42, + 1025, + syscall.Getpagesize() + 1, + 32769, + } + t.Run("Basic", func(t *testing.T) { + for _, size := range sizes { + t.Run(strconv.Itoa(size), func(t *testing.T) { + testCopyFileRange(t, int64(size), -1) + }) + } + }) + t.Run("Limited", func(t *testing.T) { + t.Run("OneLess", func(t *testing.T) { + for _, size := range sizes { + t.Run(strconv.Itoa(size), func(t *testing.T) { + testCopyFileRange(t, int64(size), int64(size)-1) + }) + } + }) + t.Run("Half", func(t *testing.T) { + for _, size := range sizes { + t.Run(strconv.Itoa(size), func(t *testing.T) { + testCopyFileRange(t, int64(size), int64(size)/2) + }) + } + }) + t.Run("More", func(t *testing.T) { + for _, size := range sizes { + t.Run(strconv.Itoa(size), func(t *testing.T) { + testCopyFileRange(t, int64(size), int64(size)+7) + }) + } + }) + }) + t.Run("DoesntTryInAppendMode", func(t *testing.T) { + dst, src, data, hook := newCopyFileRangeTest(t, 42) + + dst2, err := OpenFile(dst.Name(), O_RDWR|O_APPEND, 0755) + if err != nil { + t.Fatal(err) + } + defer dst2.Close() + + if _, err := io.Copy(dst2, src); err != nil { + t.Fatal(err) + } + if hook.called { + t.Fatal("called poll.CopyFileRange for destination in O_APPEND mode") + } + mustSeekStart(t, dst2) + mustContainData(t, dst2, data) // through traditional means + }) + t.Run("CopyFileItself", func(t *testing.T) { + hook := hookCopyFileRange(t) + + f, err := CreateTemp("", "file-readfrom-itself-test") + if err != nil { + t.Fatalf("failed to create tmp file: %v", err) + } + t.Cleanup(func() { + f.Close() + Remove(f.Name()) + }) + + data := []byte("hello world!") + if _, err := f.Write(data); err != nil { + t.Fatalf("failed to create and feed the file: %v", err) + } + + if err := f.Sync(); err != nil { + t.Fatalf("failed to save the file: %v", err) + } + + // Rewind it. + if _, err := f.Seek(0, io.SeekStart); err != nil { + t.Fatalf("failed to rewind the file: %v", err) + } + + // Read data from the file itself. + if _, err := io.Copy(f, f); err != nil { + t.Fatalf("failed to read from the file: %v", err) + } + + if !hook.called || hook.written != 0 || hook.handled || hook.err != nil { + t.Fatalf("poll.CopyFileRange should be called and return the EINVAL error, but got hook.called=%t, hook.err=%v", hook.called, hook.err) + } + + // Rewind it. + if _, err := f.Seek(0, io.SeekStart); err != nil { + t.Fatalf("failed to rewind the file: %v", err) + } + + data2, err := io.ReadAll(f) + if err != nil { + t.Fatalf("failed to read from the file: %v", err) + } + + // It should wind up a double of the original data. + if strings.Repeat(string(data), 2) != string(data2) { + t.Fatalf("data mismatch: %s != %s", string(data), string(data2)) + } + }) + t.Run("NotRegular", func(t *testing.T) { + t.Run("BothPipes", func(t *testing.T) { + hook := hookCopyFileRange(t) + + pr1, pw1, err := Pipe() + if err != nil { + t.Fatal(err) + } + defer pr1.Close() + defer pw1.Close() + + pr2, pw2, err := Pipe() + if err != nil { + t.Fatal(err) + } + defer pr2.Close() + defer pw2.Close() + + // The pipe is empty, and PIPE_BUF is large enough + // for this, by (POSIX) definition, so there is no + // need for an additional goroutine. + data := []byte("hello") + if _, err := pw1.Write(data); err != nil { + t.Fatal(err) + } + pw1.Close() + + n, err := io.Copy(pw2, pr1) + if err != nil { + t.Fatal(err) + } + if n != int64(len(data)) { + t.Fatalf("transferred %d, want %d", n, len(data)) + } + if !hook.called { + t.Fatalf("should have called poll.CopyFileRange") + } + pw2.Close() + mustContainData(t, pr2, data) + }) + t.Run("DstPipe", func(t *testing.T) { + dst, src, data, hook := newCopyFileRangeTest(t, 255) + dst.Close() + + pr, pw, err := Pipe() + if err != nil { + t.Fatal(err) + } + defer pr.Close() + defer pw.Close() + + n, err := io.Copy(pw, src) + if err != nil { + t.Fatal(err) + } + if n != int64(len(data)) { + t.Fatalf("transferred %d, want %d", n, len(data)) + } + if !hook.called { + t.Fatalf("should have called poll.CopyFileRange") + } + pw.Close() + mustContainData(t, pr, data) + }) + t.Run("SrcPipe", func(t *testing.T) { + dst, src, data, hook := newCopyFileRangeTest(t, 255) + src.Close() + + pr, pw, err := Pipe() + if err != nil { + t.Fatal(err) + } + defer pr.Close() + defer pw.Close() + + // The pipe is empty, and PIPE_BUF is large enough + // for this, by (POSIX) definition, so there is no + // need for an additional goroutine. + if _, err := pw.Write(data); err != nil { + t.Fatal(err) + } + pw.Close() + + n, err := io.Copy(dst, pr) + if err != nil { + t.Fatal(err) + } + if n != int64(len(data)) { + t.Fatalf("transferred %d, want %d", n, len(data)) + } + if !hook.called { + t.Fatalf("should have called poll.CopyFileRange") + } + mustSeekStart(t, dst) + mustContainData(t, dst, data) + }) + }) + t.Run("Nil", func(t *testing.T) { + var nilFile *File + anyFile, err := CreateTemp("", "") + if err != nil { + t.Fatal(err) + } + defer Remove(anyFile.Name()) + defer anyFile.Close() + + if _, err := io.Copy(nilFile, nilFile); err != ErrInvalid { + t.Errorf("io.Copy(nilFile, nilFile) = %v, want %v", err, ErrInvalid) + } + if _, err := io.Copy(anyFile, nilFile); err != ErrInvalid { + t.Errorf("io.Copy(anyFile, nilFile) = %v, want %v", err, ErrInvalid) + } + if _, err := io.Copy(nilFile, anyFile); err != ErrInvalid { + t.Errorf("io.Copy(nilFile, anyFile) = %v, want %v", err, ErrInvalid) + } + + if _, err := nilFile.ReadFrom(nilFile); err != ErrInvalid { + t.Errorf("nilFile.ReadFrom(nilFile) = %v, want %v", err, ErrInvalid) + } + if _, err := anyFile.ReadFrom(nilFile); err != ErrInvalid { + t.Errorf("anyFile.ReadFrom(nilFile) = %v, want %v", err, ErrInvalid) + } + if _, err := nilFile.ReadFrom(anyFile); err != ErrInvalid { + t.Errorf("nilFile.ReadFrom(anyFile) = %v, want %v", err, ErrInvalid) + } + }) +} + +func TestSpliceFile(t *testing.T) { + sizes := []int{ + 1, + 42, + 1025, + syscall.Getpagesize() + 1, + 32769, + } + t.Run("Basic-TCP", func(t *testing.T) { + for _, size := range sizes { + t.Run(strconv.Itoa(size), func(t *testing.T) { + testSpliceFile(t, "tcp", int64(size), -1) + }) + } + }) + t.Run("Basic-Unix", func(t *testing.T) { + for _, size := range sizes { + t.Run(strconv.Itoa(size), func(t *testing.T) { + testSpliceFile(t, "unix", int64(size), -1) + }) + } + }) + t.Run("TCP-To-TTY", func(t *testing.T) { + testSpliceToTTY(t, "tcp", 32768) + }) + t.Run("Unix-To-TTY", func(t *testing.T) { + testSpliceToTTY(t, "unix", 32768) + }) + t.Run("Limited", func(t *testing.T) { + t.Run("OneLess-TCP", func(t *testing.T) { + for _, size := range sizes { + t.Run(strconv.Itoa(size), func(t *testing.T) { + testSpliceFile(t, "tcp", int64(size), int64(size)-1) + }) + } + }) + t.Run("OneLess-Unix", func(t *testing.T) { + for _, size := range sizes { + t.Run(strconv.Itoa(size), func(t *testing.T) { + testSpliceFile(t, "unix", int64(size), int64(size)-1) + }) + } + }) + t.Run("Half-TCP", func(t *testing.T) { + for _, size := range sizes { + t.Run(strconv.Itoa(size), func(t *testing.T) { + testSpliceFile(t, "tcp", int64(size), int64(size)/2) + }) + } + }) + t.Run("Half-Unix", func(t *testing.T) { + for _, size := range sizes { + t.Run(strconv.Itoa(size), func(t *testing.T) { + testSpliceFile(t, "unix", int64(size), int64(size)/2) + }) + } + }) + t.Run("More-TCP", func(t *testing.T) { + for _, size := range sizes { + t.Run(strconv.Itoa(size), func(t *testing.T) { + testSpliceFile(t, "tcp", int64(size), int64(size)+1) + }) + } + }) + t.Run("More-Unix", func(t *testing.T) { + for _, size := range sizes { + t.Run(strconv.Itoa(size), func(t *testing.T) { + testSpliceFile(t, "unix", int64(size), int64(size)+1) + }) + } + }) + }) +} + +func testSpliceFile(t *testing.T, proto string, size, limit int64) { + dst, src, data, hook, cleanup := newSpliceFileTest(t, proto, size) + defer cleanup() + + // If we have a limit, wrap the reader. + var ( + r io.Reader + lr *io.LimitedReader + ) + if limit >= 0 { + lr = &io.LimitedReader{N: limit, R: src} + r = lr + if limit < int64(len(data)) { + data = data[:limit] + } + } else { + r = src + } + // Now call ReadFrom (through io.Copy), which will hopefully call poll.Splice + n, err := io.Copy(dst, r) + if err != nil { + t.Fatal(err) + } + + // We should have called poll.Splice with the right file descriptor arguments. + if n > 0 && !hook.called { + t.Fatal("expected to called poll.Splice") + } + if hook.called && hook.dstfd != int(dst.Fd()) { + t.Fatalf("wrong destination file descriptor: got %d, want %d", hook.dstfd, dst.Fd()) + } + sc, ok := src.(syscall.Conn) + if !ok { + t.Fatalf("server Conn is not a syscall.Conn") + } + rc, err := sc.SyscallConn() + if err != nil { + t.Fatalf("server Conn SyscallConn error: %v", err) + } + if err = rc.Control(func(fd uintptr) { + if hook.called && hook.srcfd != int(fd) { + t.Fatalf("wrong source file descriptor: got %d, want %d", hook.srcfd, int(fd)) + } + }); err != nil { + t.Fatalf("server Conn Control error: %v", err) + } + + // Check that the offsets after the transfer make sense, that the size + // of the transfer was reported correctly, and that the destination + // file contains exactly the bytes we expect it to contain. + dstoff, err := dst.Seek(0, io.SeekCurrent) + if err != nil { + t.Fatal(err) + } + if dstoff != int64(len(data)) { + t.Errorf("dstoff = %d, want %d", dstoff, len(data)) + } + if n != int64(len(data)) { + t.Errorf("short ReadFrom: wrote %d bytes, want %d", n, len(data)) + } + mustSeekStart(t, dst) + mustContainData(t, dst, data) + + // If we had a limit, check that it was updated. + if lr != nil { + if want := limit - n; lr.N != want { + t.Fatalf("didn't update limit correctly: got %d, want %d", lr.N, want) + } + } +} + +// Issue #59041. +func testSpliceToTTY(t *testing.T, proto string, size int64) { + var wg sync.WaitGroup + + // Call wg.Wait as the final deferred function, + // because the goroutines may block until some of + // the deferred Close calls. + defer wg.Wait() + + pty, ttyName, err := testpty.Open() + if err != nil { + t.Skipf("skipping test because pty open failed: %v", err) + } + defer pty.Close() + + // Open the tty directly, rather than via OpenFile. + // This bypasses the non-blocking support and is required + // to recreate the problem in the issue (#59041). + ttyFD, err := syscall.Open(ttyName, syscall.O_RDWR, 0) + if err != nil { + t.Skipf("skipping test becaused failed to open tty: %v", err) + } + defer syscall.Close(ttyFD) + + tty := NewFile(uintptr(ttyFD), "tty") + defer tty.Close() + + client, server := createSocketPair(t, proto) + + data := bytes.Repeat([]byte{'a'}, int(size)) + + wg.Add(1) + go func() { + defer wg.Done() + // The problem (issue #59041) occurs when writing + // a series of blocks of data. It does not occur + // when all the data is written at once. + for i := 0; i < len(data); i += 1024 { + if _, err := client.Write(data[i : i+1024]); err != nil { + // If we get here because the client was + // closed, skip the error. + if !errors.Is(err, net.ErrClosed) { + t.Errorf("error writing to socket: %v", err) + } + return + } + } + client.Close() + }() + + wg.Add(1) + go func() { + defer wg.Done() + buf := make([]byte, 32) + for { + if _, err := pty.Read(buf); err != nil { + if err != io.EOF && !errors.Is(err, ErrClosed) { + // An error here doesn't matter for + // our test. + t.Logf("error reading from pty: %v", err) + } + return + } + } + }() + + // Close Client to wake up the writing goroutine if necessary. + defer client.Close() + + _, err = io.Copy(tty, server) + if err != nil { + t.Fatal(err) + } +} + +func testCopyFileRange(t *testing.T, size int64, limit int64) { + dst, src, data, hook := newCopyFileRangeTest(t, size) + + // If we have a limit, wrap the reader. + var ( + realsrc io.Reader + lr *io.LimitedReader + ) + if limit >= 0 { + lr = &io.LimitedReader{N: limit, R: src} + realsrc = lr + if limit < int64(len(data)) { + data = data[:limit] + } + } else { + realsrc = src + } + + // Now call ReadFrom (through io.Copy), which will hopefully call + // poll.CopyFileRange. + n, err := io.Copy(dst, realsrc) + if err != nil { + t.Fatal(err) + } + + // If we didn't have a limit, we should have called poll.CopyFileRange + // with the right file descriptor arguments. + if limit > 0 && !hook.called { + t.Fatal("never called poll.CopyFileRange") + } + if hook.called && hook.dstfd != int(dst.Fd()) { + t.Fatalf("wrong destination file descriptor: got %d, want %d", hook.dstfd, dst.Fd()) + } + if hook.called && hook.srcfd != int(src.Fd()) { + t.Fatalf("wrong source file descriptor: got %d, want %d", hook.srcfd, src.Fd()) + } + + // Check that the offsets after the transfer make sense, that the size + // of the transfer was reported correctly, and that the destination + // file contains exactly the bytes we expect it to contain. + dstoff, err := dst.Seek(0, io.SeekCurrent) + if err != nil { + t.Fatal(err) + } + srcoff, err := src.Seek(0, io.SeekCurrent) + if err != nil { + t.Fatal(err) + } + if dstoff != srcoff { + t.Errorf("offsets differ: dstoff = %d, srcoff = %d", dstoff, srcoff) + } + if dstoff != int64(len(data)) { + t.Errorf("dstoff = %d, want %d", dstoff, len(data)) + } + if n != int64(len(data)) { + t.Errorf("short ReadFrom: wrote %d bytes, want %d", n, len(data)) + } + mustSeekStart(t, dst) + mustContainData(t, dst, data) + + // If we had a limit, check that it was updated. + if lr != nil { + if want := limit - n; lr.N != want { + t.Fatalf("didn't update limit correctly: got %d, want %d", lr.N, want) + } + } +} + +// newCopyFileRangeTest initializes a new test for copy_file_range. +// +// It creates source and destination files, and populates the source file +// with random data of the specified size. It also hooks package os' call +// to poll.CopyFileRange and returns the hook so it can be inspected. +func newCopyFileRangeTest(t *testing.T, size int64) (dst, src *File, data []byte, hook *copyFileRangeHook) { + t.Helper() + + hook = hookCopyFileRange(t) + tmp := t.TempDir() + + src, err := Create(filepath.Join(tmp, "src")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { src.Close() }) + + dst, err = Create(filepath.Join(tmp, "dst")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { dst.Close() }) + + // Populate the source file with data, then rewind it, so it can be + // consumed by copy_file_range(2). + prng := rand.New(rand.NewSource(time.Now().Unix())) + data = make([]byte, size) + prng.Read(data) + if _, err := src.Write(data); err != nil { + t.Fatal(err) + } + if _, err := src.Seek(0, io.SeekStart); err != nil { + t.Fatal(err) + } + + return dst, src, data, hook +} + +// newSpliceFileTest initializes a new test for splice. +// +// It creates source sockets and destination file, and populates the source sockets +// with random data of the specified size. It also hooks package os' call +// to poll.Splice and returns the hook so it can be inspected. +func newSpliceFileTest(t *testing.T, proto string, size int64) (*File, net.Conn, []byte, *spliceFileHook, func()) { + t.Helper() + + hook := hookSpliceFile(t) + + client, server := createSocketPair(t, proto) + + dst, err := CreateTemp(t.TempDir(), "dst-splice-file-test") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { dst.Close() }) + + randSeed := time.Now().Unix() + t.Logf("random data seed: %d\n", randSeed) + prng := rand.New(rand.NewSource(randSeed)) + data := make([]byte, size) + prng.Read(data) + + done := make(chan struct{}) + go func() { + client.Write(data) + client.Close() + close(done) + }() + + return dst, server, data, hook, func() { <-done } +} + +// mustContainData ensures that the specified file contains exactly the +// specified data. +func mustContainData(t *testing.T, f *File, data []byte) { + t.Helper() + + got := make([]byte, len(data)) + if _, err := io.ReadFull(f, got); err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, data) { + t.Fatalf("didn't get the same data back from %s", f.Name()) + } + if _, err := f.Read(make([]byte, 1)); err != io.EOF { + t.Fatalf("not at EOF") + } +} + +func mustSeekStart(t *testing.T, f *File) { + if _, err := f.Seek(0, io.SeekStart); err != nil { + t.Fatal(err) + } +} + +func hookCopyFileRange(t *testing.T) *copyFileRangeHook { + h := new(copyFileRangeHook) + h.install() + t.Cleanup(h.uninstall) + return h +} + +type copyFileRangeHook struct { + called bool + dstfd int + srcfd int + remain int64 + + written int64 + handled bool + err error + + original func(dst, src *poll.FD, remain int64) (int64, bool, error) +} + +func (h *copyFileRangeHook) install() { + h.original = *PollCopyFileRangeP + *PollCopyFileRangeP = func(dst, src *poll.FD, remain int64) (int64, bool, error) { + h.called = true + h.dstfd = dst.Sysfd + h.srcfd = src.Sysfd + h.remain = remain + h.written, h.handled, h.err = h.original(dst, src, remain) + return h.written, h.handled, h.err + } +} + +func (h *copyFileRangeHook) uninstall() { + *PollCopyFileRangeP = h.original +} + +func hookSpliceFile(t *testing.T) *spliceFileHook { + h := new(spliceFileHook) + h.install() + t.Cleanup(h.uninstall) + return h +} + +type spliceFileHook struct { + called bool + dstfd int + srcfd int + remain int64 + + written int64 + handled bool + sc string + err error + + original func(dst, src *poll.FD, remain int64) (int64, bool, string, error) +} + +func (h *spliceFileHook) install() { + h.original = *PollSpliceFile + *PollSpliceFile = func(dst, src *poll.FD, remain int64) (int64, bool, string, error) { + h.called = true + h.dstfd = dst.Sysfd + h.srcfd = src.Sysfd + h.remain = remain + h.written, h.handled, h.sc, h.err = h.original(dst, src, remain) + return h.written, h.handled, h.sc, h.err + } +} + +func (h *spliceFileHook) uninstall() { + *PollSpliceFile = h.original +} + +// On some kernels copy_file_range fails on files in /proc. +func TestProcCopy(t *testing.T) { + t.Parallel() + + const cmdlineFile = "/proc/self/cmdline" + cmdline, err := ReadFile(cmdlineFile) + if err != nil { + t.Skipf("can't read /proc file: %v", err) + } + in, err := Open(cmdlineFile) + if err != nil { + t.Fatal(err) + } + defer in.Close() + outFile := filepath.Join(t.TempDir(), "cmdline") + out, err := Create(outFile) + if err != nil { + t.Fatal(err) + } + if _, err := io.Copy(out, in); err != nil { + t.Fatal(err) + } + if err := out.Close(); err != nil { + t.Fatal(err) + } + copy, err := ReadFile(outFile) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(cmdline, copy) { + t.Errorf("copy of %q got %q want %q\n", cmdlineFile, copy, cmdline) + } +} + +func TestGetPollFDFromReader(t *testing.T) { + t.Run("tcp", func(t *testing.T) { testGetPollFromReader(t, "tcp") }) + t.Run("unix", func(t *testing.T) { testGetPollFromReader(t, "unix") }) +} + +func testGetPollFromReader(t *testing.T, proto string) { + _, server := createSocketPair(t, proto) + sc, ok := server.(syscall.Conn) + if !ok { + t.Fatalf("server Conn is not a syscall.Conn") + } + rc, err := sc.SyscallConn() + if err != nil { + t.Fatalf("server SyscallConn error: %v", err) + } + if err = rc.Control(func(fd uintptr) { + pfd := GetPollFDForTest(server) + if pfd == nil { + t.Fatalf("GetPollFDForTest didn't return poll.FD") + } + if pfd.Sysfd != int(fd) { + t.Fatalf("GetPollFDForTest returned wrong poll.FD, got: %d, want: %d", pfd.Sysfd, int(fd)) + } + if !pfd.IsStream { + t.Fatalf("expected IsStream to be true") + } + if err = pfd.Init(proto, true); err == nil { + t.Fatalf("Init should have failed with the initialized poll.FD and return EEXIST error") + } + }); err != nil { + t.Fatalf("server Control error: %v", err) + } +} + +func createSocketPair(t *testing.T, proto string) (client, server net.Conn) { + t.Helper() + if !nettest.TestableNetwork(proto) { + t.Skipf("%s does not support %q", runtime.GOOS, proto) + } + + ln, err := nettest.NewLocalListener(proto) + if err != nil { + t.Fatalf("NewLocalListener error: %v", err) + } + t.Cleanup(func() { + if ln != nil { + ln.Close() + } + if client != nil { + client.Close() + } + if server != nil { + server.Close() + } + }) + ch := make(chan struct{}) + go func() { + var err error + server, err = ln.Accept() + if err != nil { + t.Errorf("Accept new connection error: %v", err) + } + ch <- struct{}{} + }() + client, err = net.Dial(proto, ln.Addr().String()) + <-ch + if err != nil { + t.Fatalf("Dial new connection error: %v", err) + } + return client, server +} diff --git a/src/os/readfrom_stub.go b/src/os/readfrom_stub.go new file mode 100644 index 0000000..8b7d5fb --- /dev/null +++ b/src/os/readfrom_stub.go @@ -0,0 +1,13 @@ +// 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. + +//go:build !linux + +package os + +import "io" + +func (f *File) readFrom(r io.Reader) (n int64, handled bool, err error) { + return 0, false, nil +} diff --git a/src/os/removeall_at.go b/src/os/removeall_at.go new file mode 100644 index 0000000..8ea5df4 --- /dev/null +++ b/src/os/removeall_at.go @@ -0,0 +1,199 @@ +// Copyright 2018 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. + +//go:build unix + +package os + +import ( + "internal/syscall/unix" + "io" + "syscall" +) + +func removeAll(path string) error { + if path == "" { + // fail silently to retain compatibility with previous behavior + // of RemoveAll. See issue 28830. + return nil + } + + // The rmdir system call does not permit removing ".", + // so we don't permit it either. + if endsWithDot(path) { + return &PathError{Op: "RemoveAll", Path: path, Err: syscall.EINVAL} + } + + // Simple case: if Remove works, we're done. + err := Remove(path) + if err == nil || IsNotExist(err) { + return nil + } + + // RemoveAll recurses by deleting the path base from + // its parent directory + parentDir, base := splitPath(path) + + parent, err := Open(parentDir) + if IsNotExist(err) { + // If parent does not exist, base cannot exist. Fail silently + return nil + } + if err != nil { + return err + } + defer parent.Close() + + if err := removeAllFrom(parent, base); err != nil { + if pathErr, ok := err.(*PathError); ok { + pathErr.Path = parentDir + string(PathSeparator) + pathErr.Path + err = pathErr + } + return err + } + return nil +} + +func removeAllFrom(parent *File, base string) error { + parentFd := int(parent.Fd()) + // Simple case: if Unlink (aka remove) works, we're done. + err := ignoringEINTR(func() error { + return unix.Unlinkat(parentFd, base, 0) + }) + if err == nil || IsNotExist(err) { + return nil + } + + // EISDIR means that we have a directory, and we need to + // remove its contents. + // EPERM or EACCES means that we don't have write permission on + // the parent directory, but this entry might still be a directory + // whose contents need to be removed. + // Otherwise just return the error. + if err != syscall.EISDIR && err != syscall.EPERM && err != syscall.EACCES { + return &PathError{Op: "unlinkat", Path: base, Err: err} + } + + // Is this a directory we need to recurse into? + var statInfo syscall.Stat_t + statErr := ignoringEINTR(func() error { + return unix.Fstatat(parentFd, base, &statInfo, unix.AT_SYMLINK_NOFOLLOW) + }) + if statErr != nil { + if IsNotExist(statErr) { + return nil + } + return &PathError{Op: "fstatat", Path: base, Err: statErr} + } + if statInfo.Mode&syscall.S_IFMT != syscall.S_IFDIR { + // Not a directory; return the error from the unix.Unlinkat. + return &PathError{Op: "unlinkat", Path: base, Err: err} + } + + // Remove the directory's entries. + var recurseErr error + for { + const reqSize = 1024 + var respSize int + + // Open the directory to recurse into + file, err := openFdAt(parentFd, base) + if err != nil { + if IsNotExist(err) { + return nil + } + recurseErr = &PathError{Op: "openfdat", Path: base, Err: err} + break + } + + for { + numErr := 0 + + names, readErr := file.Readdirnames(reqSize) + // Errors other than EOF should stop us from continuing. + if readErr != nil && readErr != io.EOF { + file.Close() + if IsNotExist(readErr) { + return nil + } + return &PathError{Op: "readdirnames", Path: base, Err: readErr} + } + + respSize = len(names) + for _, name := range names { + err := removeAllFrom(file, name) + if err != nil { + if pathErr, ok := err.(*PathError); ok { + pathErr.Path = base + string(PathSeparator) + pathErr.Path + } + numErr++ + if recurseErr == nil { + recurseErr = err + } + } + } + + // If we can delete any entry, break to start new iteration. + // Otherwise, we discard current names, get next entries and try deleting them. + if numErr != reqSize { + break + } + } + + // Removing files from the directory may have caused + // the OS to reshuffle it. Simply calling Readdirnames + // again may skip some entries. The only reliable way + // to avoid this is to close and re-open the + // directory. See issue 20841. + file.Close() + + // Finish when the end of the directory is reached + if respSize < reqSize { + break + } + } + + // Remove the directory itself. + unlinkError := ignoringEINTR(func() error { + return unix.Unlinkat(parentFd, base, unix.AT_REMOVEDIR) + }) + if unlinkError == nil || IsNotExist(unlinkError) { + return nil + } + + if recurseErr != nil { + return recurseErr + } + return &PathError{Op: "unlinkat", Path: base, Err: unlinkError} +} + +// openFdAt opens path relative to the directory in fd. +// Other than that this should act like openFileNolog. +// This acts like openFileNolog rather than OpenFile because +// we are going to (try to) remove the file. +// The contents of this file are not relevant for test caching. +func openFdAt(dirfd int, name string) (*File, error) { + var r int + for { + var e error + r, e = unix.Openat(dirfd, name, O_RDONLY|syscall.O_CLOEXEC, 0) + if e == nil { + break + } + + // See comment in openFileNolog. + if e == syscall.EINTR { + continue + } + + return nil, e + } + + if !supportsCloseOnExec { + syscall.CloseOnExec(r) + } + + // We use kindNoPoll because we know that this is a directory. + return newFile(r, name, kindNoPoll), nil +} diff --git a/src/os/removeall_noat.go b/src/os/removeall_noat.go new file mode 100644 index 0000000..2b8a772 --- /dev/null +++ b/src/os/removeall_noat.go @@ -0,0 +1,142 @@ +// Copyright 2018 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. + +//go:build !unix + +package os + +import ( + "io" + "runtime" + "syscall" +) + +func removeAll(path string) error { + if path == "" { + // fail silently to retain compatibility with previous behavior + // of RemoveAll. See issue 28830. + return nil + } + + // The rmdir system call permits removing "." on Plan 9, + // so we don't permit it to remain consistent with the + // "at" implementation of RemoveAll. + if endsWithDot(path) { + return &PathError{Op: "RemoveAll", Path: path, Err: syscall.EINVAL} + } + + // Simple case: if Remove works, we're done. + err := Remove(path) + if err == nil || IsNotExist(err) { + return nil + } + + // Otherwise, is this a directory we need to recurse into? + dir, serr := Lstat(path) + if serr != nil { + if serr, ok := serr.(*PathError); ok && (IsNotExist(serr.Err) || serr.Err == syscall.ENOTDIR) { + return nil + } + return serr + } + if !dir.IsDir() { + // Not a directory; return the error from Remove. + return err + } + + // Remove contents & return first error. + err = nil + for { + fd, err := Open(path) + if err != nil { + if IsNotExist(err) { + // Already deleted by someone else. + return nil + } + return err + } + + const reqSize = 1024 + var names []string + var readErr error + + for { + numErr := 0 + names, readErr = fd.Readdirnames(reqSize) + + for _, name := range names { + err1 := RemoveAll(path + string(PathSeparator) + name) + if err == nil { + err = err1 + } + if err1 != nil { + numErr++ + } + } + + // If we can delete any entry, break to start new iteration. + // Otherwise, we discard current names, get next entries and try deleting them. + if numErr != reqSize { + break + } + } + + // Removing files from the directory may have caused + // the OS to reshuffle it. Simply calling Readdirnames + // again may skip some entries. The only reliable way + // to avoid this is to close and re-open the + // directory. See issue 20841. + fd.Close() + + if readErr == io.EOF { + break + } + // If Readdirnames returned an error, use it. + if err == nil { + err = readErr + } + if len(names) == 0 { + break + } + + // We don't want to re-open unnecessarily, so if we + // got fewer than request names from Readdirnames, try + // simply removing the directory now. If that + // succeeds, we are done. + if len(names) < reqSize { + err1 := Remove(path) + if err1 == nil || IsNotExist(err1) { + return nil + } + + if err != nil { + // We got some error removing the + // directory contents, and since we + // read fewer names than we requested + // there probably aren't more files to + // remove. Don't loop around to read + // the directory again. We'll probably + // just get the same error. + return err + } + } + } + + // Remove directory. + err1 := Remove(path) + if err1 == nil || IsNotExist(err1) { + return nil + } + if runtime.GOOS == "windows" && IsPermission(err1) { + if fs, err := Stat(path); err == nil { + if err = Chmod(path, FileMode(0200|int(fs.Mode()))); err == nil { + err1 = Remove(path) + } + } + } + if err == nil { + err = err1 + } + return err +} diff --git a/src/os/removeall_test.go b/src/os/removeall_test.go new file mode 100644 index 0000000..2f7938b --- /dev/null +++ b/src/os/removeall_test.go @@ -0,0 +1,506 @@ +// Copyright 2018 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 os_test + +import ( + "bytes" + "fmt" + "internal/testenv" + . "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "testing" +) + +func TestRemoveAll(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + if err := RemoveAll(""); err != nil { + t.Errorf("RemoveAll(\"\"): %v; want nil", err) + } + + file := filepath.Join(tmpDir, "file") + path := filepath.Join(tmpDir, "_TestRemoveAll_") + fpath := filepath.Join(path, "file") + dpath := filepath.Join(path, "dir") + + // Make a regular file and remove + fd, err := Create(file) + if err != nil { + t.Fatalf("create %q: %s", file, err) + } + fd.Close() + if err = RemoveAll(file); err != nil { + t.Fatalf("RemoveAll %q (first): %s", file, err) + } + if _, err = Lstat(file); err == nil { + t.Fatalf("Lstat %q succeeded after RemoveAll (first)", file) + } + + // Make directory with 1 file and remove. + if err := MkdirAll(path, 0777); err != nil { + t.Fatalf("MkdirAll %q: %s", path, err) + } + fd, err = Create(fpath) + if err != nil { + t.Fatalf("create %q: %s", fpath, err) + } + fd.Close() + if err = RemoveAll(path); err != nil { + t.Fatalf("RemoveAll %q (second): %s", path, err) + } + if _, err = Lstat(path); err == nil { + t.Fatalf("Lstat %q succeeded after RemoveAll (second)", path) + } + + // Make directory with file and subdirectory and remove. + if err = MkdirAll(dpath, 0777); err != nil { + t.Fatalf("MkdirAll %q: %s", dpath, err) + } + fd, err = Create(fpath) + if err != nil { + t.Fatalf("create %q: %s", fpath, err) + } + fd.Close() + fd, err = Create(dpath + "/file") + if err != nil { + t.Fatalf("create %q: %s", fpath, err) + } + fd.Close() + if err = RemoveAll(path); err != nil { + t.Fatalf("RemoveAll %q (third): %s", path, err) + } + if _, err := Lstat(path); err == nil { + t.Fatalf("Lstat %q succeeded after RemoveAll (third)", path) + } + + // Chmod is not supported under Windows or wasip1 and test fails as root. + if runtime.GOOS != "windows" && runtime.GOOS != "wasip1" && Getuid() != 0 { + // Make directory with file and subdirectory and trigger error. + if err = MkdirAll(dpath, 0777); err != nil { + t.Fatalf("MkdirAll %q: %s", dpath, err) + } + + for _, s := range []string{fpath, dpath + "/file1", path + "/zzz"} { + fd, err = Create(s) + if err != nil { + t.Fatalf("create %q: %s", s, err) + } + fd.Close() + } + if err = Chmod(dpath, 0); err != nil { + t.Fatalf("Chmod %q 0: %s", dpath, err) + } + + // No error checking here: either RemoveAll + // will or won't be able to remove dpath; + // either way we want to see if it removes fpath + // and path/zzz. Reasons why RemoveAll might + // succeed in removing dpath as well include: + // * running as root + // * running on a file system without permissions (FAT) + RemoveAll(path) + Chmod(dpath, 0777) + + for _, s := range []string{fpath, path + "/zzz"} { + if _, err = Lstat(s); err == nil { + t.Fatalf("Lstat %q succeeded after partial RemoveAll", s) + } + } + } + if err = RemoveAll(path); err != nil { + t.Fatalf("RemoveAll %q after partial RemoveAll: %s", path, err) + } + if _, err = Lstat(path); err == nil { + t.Fatalf("Lstat %q succeeded after RemoveAll (final)", path) + } +} + +// Test RemoveAll on a large directory. +func TestRemoveAllLarge(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + t.Parallel() + + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "_TestRemoveAllLarge_") + + // Make directory with 1000 files and remove. + if err := MkdirAll(path, 0777); err != nil { + t.Fatalf("MkdirAll %q: %s", path, err) + } + for i := 0; i < 1000; i++ { + fpath := fmt.Sprintf("%s/file%d", path, i) + fd, err := Create(fpath) + if err != nil { + t.Fatalf("create %q: %s", fpath, err) + } + fd.Close() + } + if err := RemoveAll(path); err != nil { + t.Fatalf("RemoveAll %q: %s", path, err) + } + if _, err := Lstat(path); err == nil { + t.Fatalf("Lstat %q succeeded after RemoveAll", path) + } +} + +func TestRemoveAllLongPath(t *testing.T) { + switch runtime.GOOS { + case "aix", "darwin", "ios", "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "illumos", "solaris": + break + default: + t.Skip("skipping for not implemented platforms") + } + + prevDir, err := Getwd() + if err != nil { + t.Fatalf("Could not get wd: %s", err) + } + + startPath, err := MkdirTemp("", "TestRemoveAllLongPath-") + if err != nil { + t.Fatalf("Could not create TempDir: %s", err) + } + defer RemoveAll(startPath) + + err = Chdir(startPath) + if err != nil { + t.Fatalf("Could not chdir %s: %s", startPath, err) + } + + // Removing paths with over 4096 chars commonly fails + for i := 0; i < 41; i++ { + name := strings.Repeat("a", 100) + + err = Mkdir(name, 0755) + if err != nil { + t.Fatalf("Could not mkdir %s: %s", name, err) + } + + err = Chdir(name) + if err != nil { + t.Fatalf("Could not chdir %s: %s", name, err) + } + } + + err = Chdir(prevDir) + if err != nil { + t.Fatalf("Could not chdir %s: %s", prevDir, err) + } + + err = RemoveAll(startPath) + if err != nil { + t.Errorf("RemoveAll could not remove long file path %s: %s", startPath, err) + } +} + +func TestRemoveAllDot(t *testing.T) { + prevDir, err := Getwd() + if err != nil { + t.Fatalf("Could not get wd: %s", err) + } + tempDir, err := MkdirTemp("", "TestRemoveAllDot-") + if err != nil { + t.Fatalf("Could not create TempDir: %s", err) + } + defer RemoveAll(tempDir) + + err = Chdir(tempDir) + if err != nil { + t.Fatalf("Could not chdir to tempdir: %s", err) + } + + err = RemoveAll(".") + if err == nil { + t.Errorf("RemoveAll succeed to remove .") + } + + err = Chdir(prevDir) + if err != nil { + t.Fatalf("Could not chdir %s: %s", prevDir, err) + } +} + +func TestRemoveAllDotDot(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + subdir := filepath.Join(tempDir, "x") + subsubdir := filepath.Join(subdir, "y") + if err := MkdirAll(subsubdir, 0777); err != nil { + t.Fatal(err) + } + if err := RemoveAll(filepath.Join(subsubdir, "..")); err != nil { + t.Error(err) + } + for _, dir := range []string{subsubdir, subdir} { + if _, err := Stat(dir); err == nil { + t.Errorf("%s: exists after RemoveAll", dir) + } + } +} + +// Issue #29178. +func TestRemoveReadOnlyDir(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + subdir := filepath.Join(tempDir, "x") + if err := Mkdir(subdir, 0); err != nil { + t.Fatal(err) + } + + // If an error occurs make it more likely that removing the + // temporary directory will succeed. + defer Chmod(subdir, 0777) + + if err := RemoveAll(subdir); err != nil { + t.Fatal(err) + } + + if _, err := Stat(subdir); err == nil { + t.Error("subdirectory was not removed") + } +} + +// Issue #29983. +func TestRemoveAllButReadOnlyAndPathError(t *testing.T) { + switch runtime.GOOS { + case "js", "wasip1", "windows": + t.Skipf("skipping test on %s", runtime.GOOS) + } + + if Getuid() == 0 { + t.Skip("skipping test when running as root") + } + + t.Parallel() + + tempDir := t.TempDir() + dirs := []string{ + "a", + "a/x", + "a/x/1", + "b", + "b/y", + "b/y/2", + "c", + "c/z", + "c/z/3", + } + readonly := []string{ + "b", + } + inReadonly := func(d string) bool { + for _, ro := range readonly { + if d == ro { + return true + } + dd, _ := filepath.Split(d) + if filepath.Clean(dd) == ro { + return true + } + } + return false + } + + for _, dir := range dirs { + if err := Mkdir(filepath.Join(tempDir, dir), 0777); err != nil { + t.Fatal(err) + } + } + for _, dir := range readonly { + d := filepath.Join(tempDir, dir) + if err := Chmod(d, 0555); err != nil { + t.Fatal(err) + } + + // Defer changing the mode back so that the deferred + // RemoveAll(tempDir) can succeed. + defer Chmod(d, 0777) + } + + err := RemoveAll(tempDir) + if err == nil { + t.Fatal("RemoveAll succeeded unexpectedly") + } + + // The error should be of type *PathError. + // see issue 30491 for details. + if pathErr, ok := err.(*PathError); ok { + want := filepath.Join(tempDir, "b", "y") + if pathErr.Path != want { + t.Errorf("RemoveAll(%q): err.Path=%q, want %q", tempDir, pathErr.Path, want) + } + } else { + t.Errorf("RemoveAll(%q): error has type %T, want *fs.PathError", tempDir, err) + } + + for _, dir := range dirs { + _, err := Stat(filepath.Join(tempDir, dir)) + if inReadonly(dir) { + if err != nil { + t.Errorf("file %q was deleted but should still exist", dir) + } + } else { + if err == nil { + t.Errorf("file %q still exists but should have been deleted", dir) + } + } + } +} + +func TestRemoveUnreadableDir(t *testing.T) { + switch runtime.GOOS { + case "js": + t.Skipf("skipping test on %s", runtime.GOOS) + } + + if Getuid() == 0 { + t.Skip("skipping test when running as root") + } + + t.Parallel() + + tempDir := t.TempDir() + target := filepath.Join(tempDir, "d0", "d1", "d2") + if err := MkdirAll(target, 0755); err != nil { + t.Fatal(err) + } + if err := Chmod(target, 0300); err != nil { + t.Fatal(err) + } + if err := RemoveAll(filepath.Join(tempDir, "d0")); err != nil { + t.Fatal(err) + } +} + +// Issue 29921 +func TestRemoveAllWithMoreErrorThanReqSize(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + t.Parallel() + + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "_TestRemoveAllWithMoreErrorThanReqSize_") + + // Make directory with 1025 read-only files. + if err := MkdirAll(path, 0777); err != nil { + t.Fatalf("MkdirAll %q: %s", path, err) + } + for i := 0; i < 1025; i++ { + fpath := filepath.Join(path, fmt.Sprintf("file%d", i)) + fd, err := Create(fpath) + if err != nil { + t.Fatalf("create %q: %s", fpath, err) + } + fd.Close() + } + + // Make the parent directory read-only. On some platforms, this is what + // prevents Remove from removing the files within that directory. + if err := Chmod(path, 0555); err != nil { + t.Fatal(err) + } + defer Chmod(path, 0755) + + // This call should not hang, even on a platform that disallows file deletion + // from read-only directories. + err := RemoveAll(path) + + if Getuid() == 0 { + // On many platforms, root can remove files from read-only directories. + return + } + if err == nil { + if runtime.GOOS == "windows" || runtime.GOOS == "wasip1" { + // Marking a directory as read-only in Windows does not prevent the RemoveAll + // from creating or removing files within it. + // + // For wasip1, there is no support for file permissions so we cannot prevent + // RemoveAll from removing the files. + return + } + t.Fatal("RemoveAll() = nil; want error") + } + + dir, err := Open(path) + if err != nil { + t.Fatal(err) + } + defer dir.Close() + + names, _ := dir.Readdirnames(1025) + if len(names) < 1025 { + t.Fatalf("RemoveAll() unexpectedly removed %d read-only files from that directory", 1025-len(names)) + } +} + +func TestRemoveAllNoFcntl(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + + const env = "GO_TEST_REMOVE_ALL_NO_FCNTL" + if dir := Getenv(env); dir != "" { + if err := RemoveAll(dir); err != nil { + t.Fatal(err) + } + return + } + + // Only test on Linux so that we can assume we have strace. + // The code is OS-independent so if it passes on Linux + // it should pass on other Unix systems. + if runtime.GOOS != "linux" { + t.Skipf("skipping test on %s", runtime.GOOS) + } + if _, err := Stat("/bin/strace"); err != nil { + t.Skipf("skipping test because /bin/strace not found: %v", err) + } + me, err := Executable() + if err != nil { + t.Skipf("skipping because Executable failed: %v", err) + } + + // Create 100 directories. + // The test is that we can remove them without calling fcntl + // on each one. + tmpdir := t.TempDir() + subdir := filepath.Join(tmpdir, "subdir") + if err := Mkdir(subdir, 0o755); err != nil { + t.Fatal(err) + } + for i := 0; i < 100; i++ { + subsubdir := filepath.Join(subdir, strconv.Itoa(i)) + if err := Mkdir(filepath.Join(subdir, strconv.Itoa(i)), 0o755); err != nil { + t.Fatal(err) + } + if err := WriteFile(filepath.Join(subsubdir, "file"), nil, 0o644); err != nil { + t.Fatal(err) + } + } + + cmd := testenv.Command(t, "/bin/strace", "-f", "-e", "fcntl", me, "-test.run=TestRemoveAllNoFcntl") + cmd = testenv.CleanCmdEnv(cmd) + cmd.Env = append(cmd.Env, env+"="+subdir) + out, err := cmd.CombinedOutput() + if len(out) > 0 { + t.Logf("%s", out) + } + if err != nil { + t.Fatal(err) + } + + if got := bytes.Count(out, []byte("fcntl")); got >= 100 { + t.Errorf("found %d fcntl calls, want < 100", got) + } +} diff --git a/src/os/signal/doc.go b/src/os/signal/doc.go new file mode 100644 index 0000000..a2a7525 --- /dev/null +++ b/src/os/signal/doc.go @@ -0,0 +1,232 @@ +// Copyright 2015 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 signal implements access to incoming signals. + +Signals are primarily used on Unix-like systems. For the use of this +package on Windows and Plan 9, see below. + +# Types of signals + +The signals SIGKILL and SIGSTOP may not be caught by a program, and +therefore cannot be affected by this package. + +Synchronous signals are signals triggered by errors in program +execution: SIGBUS, SIGFPE, and SIGSEGV. These are only considered +synchronous when caused by program execution, not when sent using +[os.Process.Kill] or the kill program or some similar mechanism. In +general, except as discussed below, Go programs will convert a +synchronous signal into a run-time panic. + +The remaining signals are asynchronous signals. They are not +triggered by program errors, but are instead sent from the kernel or +from some other program. + +Of the asynchronous signals, the SIGHUP signal is sent when a program +loses its controlling terminal. The SIGINT signal is sent when the +user at the controlling terminal presses the interrupt character, +which by default is ^C (Control-C). The SIGQUIT signal is sent when +the user at the controlling terminal presses the quit character, which +by default is ^\ (Control-Backslash). In general you can cause a +program to simply exit by pressing ^C, and you can cause it to exit +with a stack dump by pressing ^\. + +# Default behavior of signals in Go programs + +By default, a synchronous signal is converted into a run-time panic. A +SIGHUP, SIGINT, or SIGTERM signal causes the program to exit. A +SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGSTKFLT, SIGEMT, or SIGSYS signal +causes the program to exit with a stack dump. A SIGTSTP, SIGTTIN, or +SIGTTOU signal gets the system default behavior (these signals are +used by the shell for job control). The SIGPROF signal is handled +directly by the Go runtime to implement runtime.CPUProfile. Other +signals will be caught but no action will be taken. + +If the Go program is started with either SIGHUP or SIGINT ignored +(signal handler set to SIG_IGN), they will remain ignored. + +If the Go program is started with a non-empty signal mask, that will +generally be honored. However, some signals are explicitly unblocked: +the synchronous signals, SIGILL, SIGTRAP, SIGSTKFLT, SIGCHLD, SIGPROF, +and, on Linux, signals 32 (SIGCANCEL) and 33 (SIGSETXID) +(SIGCANCEL and SIGSETXID are used internally by glibc). Subprocesses +started by [os.Exec], or by [os/exec], will inherit the +modified signal mask. + +# Changing the behavior of signals in Go programs + +The functions in this package allow a program to change the way Go +programs handle signals. + +Notify disables the default behavior for a given set of asynchronous +signals and instead delivers them over one or more registered +channels. Specifically, it applies to the signals SIGHUP, SIGINT, +SIGQUIT, SIGABRT, and SIGTERM. It also applies to the job control +signals SIGTSTP, SIGTTIN, and SIGTTOU, in which case the system +default behavior does not occur. It also applies to some signals that +otherwise cause no action: SIGUSR1, SIGUSR2, SIGPIPE, SIGALRM, +SIGCHLD, SIGCONT, SIGURG, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGWINCH, +SIGIO, SIGPWR, SIGSYS, SIGINFO, SIGTHR, SIGWAITING, SIGLWP, SIGFREEZE, +SIGTHAW, SIGLOST, SIGXRES, SIGJVM1, SIGJVM2, and any real time signals +used on the system. Note that not all of these signals are available +on all systems. + +If the program was started with SIGHUP or SIGINT ignored, and Notify +is called for either signal, a signal handler will be installed for +that signal and it will no longer be ignored. If, later, Reset or +Ignore is called for that signal, or Stop is called on all channels +passed to Notify for that signal, the signal will once again be +ignored. Reset will restore the system default behavior for the +signal, while Ignore will cause the system to ignore the signal +entirely. + +If the program is started with a non-empty signal mask, some signals +will be explicitly unblocked as described above. If Notify is called +for a blocked signal, it will be unblocked. If, later, Reset is +called for that signal, or Stop is called on all channels passed to +Notify for that signal, the signal will once again be blocked. + +# SIGPIPE + +When a Go program writes to a broken pipe, the kernel will raise a +SIGPIPE signal. + +If the program has not called Notify to receive SIGPIPE signals, then +the behavior depends on the file descriptor number. A write to a +broken pipe on file descriptors 1 or 2 (standard output or standard +error) will cause the program to exit with a SIGPIPE signal. A write +to a broken pipe on some other file descriptor will take no action on +the SIGPIPE signal, and the write will fail with an EPIPE error. + +If the program has called Notify to receive SIGPIPE signals, the file +descriptor number does not matter. The SIGPIPE signal will be +delivered to the Notify channel, and the write will fail with an EPIPE +error. + +This means that, by default, command line programs will behave like +typical Unix command line programs, while other programs will not +crash with SIGPIPE when writing to a closed network connection. + +# Go programs that use cgo or SWIG + +In a Go program that includes non-Go code, typically C/C++ code +accessed using cgo or SWIG, Go's startup code normally runs first. It +configures the signal handlers as expected by the Go runtime, before +the non-Go startup code runs. If the non-Go startup code wishes to +install its own signal handlers, it must take certain steps to keep Go +working well. This section documents those steps and the overall +effect changes to signal handler settings by the non-Go code can have +on Go programs. In rare cases, the non-Go code may run before the Go +code, in which case the next section also applies. + +If the non-Go code called by the Go program does not change any signal +handlers or masks, then the behavior is the same as for a pure Go +program. + +If the non-Go code installs any signal handlers, it must use the +SA_ONSTACK flag with sigaction. Failing to do so is likely to cause +the program to crash if the signal is received. Go programs routinely +run with a limited stack, and therefore set up an alternate signal +stack. + +If the non-Go code installs a signal handler for any of the +synchronous signals (SIGBUS, SIGFPE, SIGSEGV), then it should record +the existing Go signal handler. If those signals occur while +executing Go code, it should invoke the Go signal handler (whether the +signal occurs while executing Go code can be determined by looking at +the PC passed to the signal handler). Otherwise some Go run-time +panics will not occur as expected. + +If the non-Go code installs a signal handler for any of the +asynchronous signals, it may invoke the Go signal handler or not as it +chooses. Naturally, if it does not invoke the Go signal handler, the +Go behavior described above will not occur. This can be an issue with +the SIGPROF signal in particular. + +The non-Go code should not change the signal mask on any threads +created by the Go runtime. If the non-Go code starts new threads of +its own, it may set the signal mask as it pleases. + +If the non-Go code starts a new thread, changes the signal mask, and +then invokes a Go function in that thread, the Go runtime will +automatically unblock certain signals: the synchronous signals, +SIGILL, SIGTRAP, SIGSTKFLT, SIGCHLD, SIGPROF, SIGCANCEL, and +SIGSETXID. When the Go function returns, the non-Go signal mask will +be restored. + +If the Go signal handler is invoked on a non-Go thread not running Go +code, the handler generally forwards the signal to the non-Go code, as +follows. If the signal is SIGPROF, the Go handler does +nothing. Otherwise, the Go handler removes itself, unblocks the +signal, and raises it again, to invoke any non-Go handler or default +system handler. If the program does not exit, the Go handler then +reinstalls itself and continues execution of the program. + +If a SIGPIPE signal is received, the Go program will invoke the +special handling described above if the SIGPIPE is received on a Go +thread. If the SIGPIPE is received on a non-Go thread the signal will +be forwarded to the non-Go handler, if any; if there is none the +default system handler will cause the program to terminate. + +# Non-Go programs that call Go code + +When Go code is built with options like -buildmode=c-shared, it will +be run as part of an existing non-Go program. The non-Go code may +have already installed signal handlers when the Go code starts (that +may also happen in unusual cases when using cgo or SWIG; in that case, +the discussion here applies). For -buildmode=c-archive the Go runtime +will initialize signals at global constructor time. For +-buildmode=c-shared the Go runtime will initialize signals when the +shared library is loaded. + +If the Go runtime sees an existing signal handler for the SIGCANCEL or +SIGSETXID signals (which are used only on Linux), it will turn on +the SA_ONSTACK flag and otherwise keep the signal handler. + +For the synchronous signals and SIGPIPE, the Go runtime will install a +signal handler. It will save any existing signal handler. If a +synchronous signal arrives while executing non-Go code, the Go runtime +will invoke the existing signal handler instead of the Go signal +handler. + +Go code built with -buildmode=c-archive or -buildmode=c-shared will +not install any other signal handlers by default. If there is an +existing signal handler, the Go runtime will turn on the SA_ONSTACK +flag and otherwise keep the signal handler. If Notify is called for an +asynchronous signal, a Go signal handler will be installed for that +signal. If, later, Reset is called for that signal, the original +handling for that signal will be reinstalled, restoring the non-Go +signal handler if any. + +Go code built without -buildmode=c-archive or -buildmode=c-shared will +install a signal handler for the asynchronous signals listed above, +and save any existing signal handler. If a signal is delivered to a +non-Go thread, it will act as described above, except that if there is +an existing non-Go signal handler, that handler will be installed +before raising the signal. + +# Windows + +On Windows a ^C (Control-C) or ^BREAK (Control-Break) normally cause +the program to exit. If Notify is called for [os.Interrupt], ^C or ^BREAK +will cause [os.Interrupt] to be sent on the channel, and the program will +not exit. If Reset is called, or Stop is called on all channels passed +to Notify, then the default behavior will be restored. + +Additionally, if Notify is called, and Windows sends CTRL_CLOSE_EVENT, +CTRL_LOGOFF_EVENT or CTRL_SHUTDOWN_EVENT to the process, Notify will +return syscall.SIGTERM. Unlike Control-C and Control-Break, Notify does +not change process behavior when either CTRL_CLOSE_EVENT, +CTRL_LOGOFF_EVENT or CTRL_SHUTDOWN_EVENT is received - the process will +still get terminated unless it exits. But receiving syscall.SIGTERM will +give the process an opportunity to clean up before termination. + +# Plan 9 + +On Plan 9, signals have type syscall.Note, which is a string. Calling +Notify with a syscall.Note will cause that value to be sent on the +channel when that string is posted as a note. +*/ +package signal diff --git a/src/os/signal/example_test.go b/src/os/signal/example_test.go new file mode 100644 index 0000000..ecefc75 --- /dev/null +++ b/src/os/signal/example_test.go @@ -0,0 +1,38 @@ +// Copyright 2013 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 signal_test + +import ( + "fmt" + "os" + "os/signal" +) + +func ExampleNotify() { + // Set up channel on which to send signal notifications. + // We must use a buffered channel or risk missing the signal + // if we're not ready to receive when the signal is sent. + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + // Block until a signal is received. + s := <-c + fmt.Println("Got signal:", s) +} + +func ExampleNotify_allSignals() { + // Set up channel on which to send signal notifications. + // We must use a buffered channel or risk missing the signal + // if we're not ready to receive when the signal is sent. + c := make(chan os.Signal, 1) + + // Passing no signals to Notify means that + // all signals will be sent to the channel. + signal.Notify(c) + + // Block until any signal is received. + s := <-c + fmt.Println("Got signal:", s) +} diff --git a/src/os/signal/example_unix_test.go b/src/os/signal/example_unix_test.go new file mode 100644 index 0000000..b7047ac --- /dev/null +++ b/src/os/signal/example_unix_test.go @@ -0,0 +1,47 @@ +// 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. + +//go:build unix + +package signal_test + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "time" +) + +// This example passes a context with a signal to tell a blocking function that +// it should abandon its work after a signal is received. +func ExampleNotifyContext() { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + p, err := os.FindProcess(os.Getpid()) + if err != nil { + log.Fatal(err) + } + + // On a Unix-like system, pressing Ctrl+C on a keyboard sends a + // SIGINT signal to the process of the program in execution. + // + // This example simulates that by sending a SIGINT signal to itself. + if err := p.Signal(os.Interrupt); err != nil { + log.Fatal(err) + } + + select { + case <-time.After(time.Second): + fmt.Println("missed signal") + case <-ctx.Done(): + fmt.Println(ctx.Err()) // prints "context canceled" + stop() // stop receiving signal notifications as soon as possible. + } + + // Output: + // context canceled +} diff --git a/src/os/signal/sig.s b/src/os/signal/sig.s new file mode 100644 index 0000000..12833a8 --- /dev/null +++ b/src/os/signal/sig.s @@ -0,0 +1,8 @@ +// Copyright 2012 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. + +// The runtime package uses //go:linkname to push a few functions into this +// package but we still need a .s file so the Go tool does not pass -complete +// to the go tool compile so the latter does not complain about Go functions +// with no bodies. diff --git a/src/os/signal/signal.go b/src/os/signal/signal.go new file mode 100644 index 0000000..4250a7e --- /dev/null +++ b/src/os/signal/signal.go @@ -0,0 +1,334 @@ +// Copyright 2012 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 signal + +import ( + "context" + "os" + "sync" +) + +var handlers struct { + sync.Mutex + // Map a channel to the signals that should be sent to it. + m map[chan<- os.Signal]*handler + // Map a signal to the number of channels receiving it. + ref [numSig]int64 + // Map channels to signals while the channel is being stopped. + // Not a map because entries live here only very briefly. + // We need a separate container because we need m to correspond to ref + // at all times, and we also need to keep track of the *handler + // value for a channel being stopped. See the Stop function. + stopping []stopping +} + +type stopping struct { + c chan<- os.Signal + h *handler +} + +type handler struct { + mask [(numSig + 31) / 32]uint32 +} + +func (h *handler) want(sig int) bool { + return (h.mask[sig/32]>>uint(sig&31))&1 != 0 +} + +func (h *handler) set(sig int) { + h.mask[sig/32] |= 1 << uint(sig&31) +} + +func (h *handler) clear(sig int) { + h.mask[sig/32] &^= 1 << uint(sig&31) +} + +// Stop relaying the signals, sigs, to any channels previously registered to +// receive them and either reset the signal handlers to their original values +// (action=disableSignal) or ignore the signals (action=ignoreSignal). +func cancel(sigs []os.Signal, action func(int)) { + handlers.Lock() + defer handlers.Unlock() + + remove := func(n int) { + var zerohandler handler + + for c, h := range handlers.m { + if h.want(n) { + handlers.ref[n]-- + h.clear(n) + if h.mask == zerohandler.mask { + delete(handlers.m, c) + } + } + } + + action(n) + } + + if len(sigs) == 0 { + for n := 0; n < numSig; n++ { + remove(n) + } + } else { + for _, s := range sigs { + remove(signum(s)) + } + } +} + +// Ignore causes the provided signals to be ignored. If they are received by +// the program, nothing will happen. Ignore undoes the effect of any prior +// calls to Notify for the provided signals. +// If no signals are provided, all incoming signals will be ignored. +func Ignore(sig ...os.Signal) { + cancel(sig, ignoreSignal) +} + +// Ignored reports whether sig is currently ignored. +func Ignored(sig os.Signal) bool { + sn := signum(sig) + return sn >= 0 && signalIgnored(sn) +} + +var ( + // watchSignalLoopOnce guards calling the conditionally + // initialized watchSignalLoop. If watchSignalLoop is non-nil, + // it will be run in a goroutine lazily once Notify is invoked. + // See Issue 21576. + watchSignalLoopOnce sync.Once + watchSignalLoop func() +) + +// Notify causes package signal to relay incoming signals to c. +// If no signals are provided, all incoming signals will be relayed to c. +// Otherwise, just the provided signals will. +// +// Package signal will not block sending to c: the caller must ensure +// that c has sufficient buffer space to keep up with the expected +// signal rate. For a channel used for notification of just one signal value, +// a buffer of size 1 is sufficient. +// +// It is allowed to call Notify multiple times with the same channel: +// each call expands the set of signals sent to that channel. +// The only way to remove signals from the set is to call Stop. +// +// It is allowed to call Notify multiple times with different channels +// and the same signals: each channel receives copies of incoming +// signals independently. +func Notify(c chan<- os.Signal, sig ...os.Signal) { + if c == nil { + panic("os/signal: Notify using nil channel") + } + + handlers.Lock() + defer handlers.Unlock() + + h := handlers.m[c] + if h == nil { + if handlers.m == nil { + handlers.m = make(map[chan<- os.Signal]*handler) + } + h = new(handler) + handlers.m[c] = h + } + + add := func(n int) { + if n < 0 { + return + } + if !h.want(n) { + h.set(n) + if handlers.ref[n] == 0 { + enableSignal(n) + + // The runtime requires that we enable a + // signal before starting the watcher. + watchSignalLoopOnce.Do(func() { + if watchSignalLoop != nil { + go watchSignalLoop() + } + }) + } + handlers.ref[n]++ + } + } + + if len(sig) == 0 { + for n := 0; n < numSig; n++ { + add(n) + } + } else { + for _, s := range sig { + add(signum(s)) + } + } +} + +// Reset undoes the effect of any prior calls to Notify for the provided +// signals. +// If no signals are provided, all signal handlers will be reset. +func Reset(sig ...os.Signal) { + cancel(sig, disableSignal) +} + +// Stop causes package signal to stop relaying incoming signals to c. +// It undoes the effect of all prior calls to Notify using c. +// When Stop returns, it is guaranteed that c will receive no more signals. +func Stop(c chan<- os.Signal) { + handlers.Lock() + + h := handlers.m[c] + if h == nil { + handlers.Unlock() + return + } + delete(handlers.m, c) + + for n := 0; n < numSig; n++ { + if h.want(n) { + handlers.ref[n]-- + if handlers.ref[n] == 0 { + disableSignal(n) + } + } + } + + // Signals will no longer be delivered to the channel. + // We want to avoid a race for a signal such as SIGINT: + // it should be either delivered to the channel, + // or the program should take the default action (that is, exit). + // To avoid the possibility that the signal is delivered, + // and the signal handler invoked, and then Stop deregisters + // the channel before the process function below has a chance + // to send it on the channel, put the channel on a list of + // channels being stopped and wait for signal delivery to + // quiesce before fully removing it. + + handlers.stopping = append(handlers.stopping, stopping{c, h}) + + handlers.Unlock() + + signalWaitUntilIdle() + + handlers.Lock() + + for i, s := range handlers.stopping { + if s.c == c { + handlers.stopping = append(handlers.stopping[:i], handlers.stopping[i+1:]...) + break + } + } + + handlers.Unlock() +} + +// Wait until there are no more signals waiting to be delivered. +// Defined by the runtime package. +func signalWaitUntilIdle() + +func process(sig os.Signal) { + n := signum(sig) + if n < 0 { + return + } + + handlers.Lock() + defer handlers.Unlock() + + for c, h := range handlers.m { + if h.want(n) { + // send but do not block for it + select { + case c <- sig: + default: + } + } + } + + // Avoid the race mentioned in Stop. + for _, d := range handlers.stopping { + if d.h.want(n) { + select { + case d.c <- sig: + default: + } + } + } +} + +// NotifyContext returns a copy of the parent context that is marked done +// (its Done channel is closed) when one of the listed signals arrives, +// when the returned stop function is called, or when the parent context's +// Done channel is closed, whichever happens first. +// +// The stop function unregisters the signal behavior, which, like signal.Reset, +// may restore the default behavior for a given signal. For example, the default +// behavior of a Go program receiving os.Interrupt is to exit. Calling +// NotifyContext(parent, os.Interrupt) will change the behavior to cancel +// the returned context. Future interrupts received will not trigger the default +// (exit) behavior until the returned stop function is called. +// +// The stop function releases resources associated with it, so code should +// call stop as soon as the operations running in this Context complete and +// signals no longer need to be diverted to the context. +func NotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) { + ctx, cancel := context.WithCancel(parent) + c := &signalCtx{ + Context: ctx, + cancel: cancel, + signals: signals, + } + c.ch = make(chan os.Signal, 1) + Notify(c.ch, c.signals...) + if ctx.Err() == nil { + go func() { + select { + case <-c.ch: + c.cancel() + case <-c.Done(): + } + }() + } + return c, c.stop +} + +type signalCtx struct { + context.Context + + cancel context.CancelFunc + signals []os.Signal + ch chan os.Signal +} + +func (c *signalCtx) stop() { + c.cancel() + Stop(c.ch) +} + +type stringer interface { + String() string +} + +func (c *signalCtx) String() string { + var buf []byte + // We know that the type of c.Context is context.cancelCtx, and we know that the + // String method of cancelCtx returns a string that ends with ".WithCancel". + name := c.Context.(stringer).String() + name = name[:len(name)-len(".WithCancel")] + buf = append(buf, "signal.NotifyContext("+name...) + if len(c.signals) != 0 { + buf = append(buf, ", ["...) + for i, s := range c.signals { + buf = append(buf, s.String()...) + if i != len(c.signals)-1 { + buf = append(buf, ' ') + } + } + buf = append(buf, ']') + } + buf = append(buf, ')') + return string(buf) +} diff --git a/src/os/signal/signal_cgo_test.go b/src/os/signal/signal_cgo_test.go new file mode 100644 index 0000000..ac59215 --- /dev/null +++ b/src/os/signal/signal_cgo_test.go @@ -0,0 +1,350 @@ +// Copyright 2017 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. + +//go:build (darwin || dragonfly || freebsd || (linux && !android) || netbsd || openbsd) && cgo + +// Note that this test does not work on Solaris: issue #22849. +// Don't run the test on Android because at least some versions of the +// C library do not define the posix_openpt function. + +package signal_test + +import ( + "context" + "encoding/binary" + "fmt" + "internal/testpty" + "os" + "os/exec" + "os/signal" + "runtime" + "strconv" + "syscall" + "testing" + "time" + "unsafe" +) + +const ( + ptyFD = 3 // child end of pty. + controlFD = 4 // child end of control pipe. +) + +// TestTerminalSignal tests that read from a pseudo-terminal does not return an +// error if the process is SIGSTOP'd and put in the background during the read. +// +// This test simulates stopping a Go process running in a shell with ^Z and +// then resuming with `fg`. +// +// This is a regression test for https://go.dev/issue/22838. On Darwin, PTY +// reads return EINTR when this occurs, and Go should automatically retry. +func TestTerminalSignal(t *testing.T) { + // This test simulates stopping a Go process running in a shell with ^Z + // and then resuming with `fg`. This sounds simple, but is actually + // quite complicated. + // + // In principle, what we are doing is: + // 1. Creating a new PTY parent/child FD pair. + // 2. Create a child that is in the foreground process group of the PTY, and read() from that process. + // 3. Stop the child with ^Z. + // 4. Take over as foreground process group of the PTY from the parent. + // 5. Make the child foreground process group again. + // 6. Continue the child. + // + // On Darwin, step 4 results in the read() returning EINTR once the + // process continues. internal/poll should automatically retry the + // read. + // + // These steps are complicated by the rules around foreground process + // groups. A process group cannot be foreground if it is "orphaned", + // unless it masks SIGTTOU. i.e., to be foreground the process group + // must have a parent process group in the same session or mask SIGTTOU + // (which we do). An orphaned process group cannot receive + // terminal-generated SIGTSTP at all. + // + // Achieving this requires three processes total: + // - Top-level process: this is the main test process and creates the + // pseudo-terminal. + // - GO_TEST_TERMINAL_SIGNALS=1: This process creates a new process + // group and session. The PTY is the controlling terminal for this + // session. This process masks SIGTTOU, making it eligible to be a + // foreground process group. This process will take over as foreground + // from subprocess 2 (step 4 above). + // - GO_TEST_TERMINAL_SIGNALS=2: This process create a child process + // group of subprocess 1, and is the original foreground process group + // for the PTY. This subprocess is the one that is SIGSTOP'd. + + if runtime.GOOS == "dragonfly" { + t.Skip("skipping: wait hangs on dragonfly; see https://go.dev/issue/56132") + } + + scale := 1 + if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { + if sc, err := strconv.Atoi(s); err == nil { + scale = sc + } + } + pause := time.Duration(scale) * 10 * time.Millisecond + + lvl := os.Getenv("GO_TEST_TERMINAL_SIGNALS") + switch lvl { + case "": + // Main test process, run code below. + break + case "1": + runSessionLeader(pause) + panic("unreachable") + case "2": + runStoppingChild() + panic("unreachable") + default: + fmt.Fprintf(os.Stderr, "unknown subprocess level %s\n", lvl) + os.Exit(1) + } + + t.Parallel() + + pty, procTTYName, err := testpty.Open() + if err != nil { + ptyErr := err.(*testpty.PtyError) + if ptyErr.FuncName == "posix_openpt" && ptyErr.Errno == syscall.EACCES { + t.Skip("posix_openpt failed with EACCES, assuming chroot and skipping") + } + t.Fatal(err) + } + defer pty.Close() + procTTY, err := os.OpenFile(procTTYName, os.O_RDWR, 0) + if err != nil { + t.Fatal(err) + } + defer procTTY.Close() + + // Control pipe. GO_TEST_TERMINAL_SIGNALS=2 send the PID of + // GO_TEST_TERMINAL_SIGNALS=3 here. After SIGSTOP, it also writes a + // byte to indicate that the foreground cycling is complete. + controlR, controlW, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=TestTerminalSignal") + cmd.Env = append(os.Environ(), "GO_TEST_TERMINAL_SIGNALS=1") + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout // for logging + cmd.Stderr = os.Stderr + cmd.ExtraFiles = []*os.File{procTTY, controlW} + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + Setctty: true, + Ctty: ptyFD, + } + + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + if err := procTTY.Close(); err != nil { + t.Errorf("closing procTTY: %v", err) + } + + if err := controlW.Close(); err != nil { + t.Errorf("closing controlW: %v", err) + } + + // Wait for first child to send the second child's PID. + b := make([]byte, 8) + n, err := controlR.Read(b) + if err != nil { + t.Fatalf("error reading child pid: %v\n", err) + } + if n != 8 { + t.Fatalf("unexpected short read n = %d\n", n) + } + pid := binary.LittleEndian.Uint64(b[:]) + process, err := os.FindProcess(int(pid)) + if err != nil { + t.Fatalf("unable to find child process: %v", err) + } + + // Wait for the third child to write a byte indicating that it is + // entering the read. + b = make([]byte, 1) + _, err = pty.Read(b) + if err != nil { + t.Fatalf("error reading from child: %v", err) + } + + // Give the program time to enter the read call. + // It doesn't matter much if we occasionally don't wait long enough; + // we won't be testing what we want to test, but the overall test + // will pass. + time.Sleep(pause) + + t.Logf("Sending ^Z...") + + // Send a ^Z to stop the program. + if _, err := pty.Write([]byte{26}); err != nil { + t.Fatalf("writing ^Z to pty: %v", err) + } + + // Wait for subprocess 1 to cycle the foreground process group. + if _, err := controlR.Read(b); err != nil { + t.Fatalf("error reading readiness: %v", err) + } + + t.Logf("Sending SIGCONT...") + + // Restart the stopped program. + if err := process.Signal(syscall.SIGCONT); err != nil { + t.Fatalf("Signal(SIGCONT) got err %v want nil", err) + } + + // Write some data for the program to read, which should cause it to + // exit. + if _, err := pty.Write([]byte{'\n'}); err != nil { + t.Fatalf("writing %q to pty: %v", "\n", err) + } + + t.Logf("Waiting for exit...") + + if err = cmd.Wait(); err != nil { + t.Errorf("subprogram failed: %v", err) + } +} + +// GO_TEST_TERMINAL_SIGNALS=1 subprocess above. +func runSessionLeader(pause time.Duration) { + // "Attempts to use tcsetpgrp() from a process which is a + // member of a background process group on a fildes associated + // with its controlling terminal shall cause the process group + // to be sent a SIGTTOU signal. If the calling thread is + // blocking SIGTTOU signals or the process is ignoring SIGTTOU + // signals, the process shall be allowed to perform the + // operation, and no signal is sent." + // -https://pubs.opengroup.org/onlinepubs/9699919799/functions/tcsetpgrp.html + // + // We are changing the terminal to put us in the foreground, so + // we must ignore SIGTTOU. We are also an orphaned process + // group (see above), so we must mask SIGTTOU to be eligible to + // become foreground at all. + signal.Ignore(syscall.SIGTTOU) + + pty := os.NewFile(ptyFD, "pty") + controlW := os.NewFile(controlFD, "control-pipe") + + // Slightly shorter timeout than in the parent. + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=TestTerminalSignal") + cmd.Env = append(os.Environ(), "GO_TEST_TERMINAL_SIGNALS=2") + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.ExtraFiles = []*os.File{pty} + cmd.SysProcAttr = &syscall.SysProcAttr{ + Foreground: true, + Ctty: ptyFD, + } + if err := cmd.Start(); err != nil { + fmt.Fprintf(os.Stderr, "error starting second subprocess: %v\n", err) + os.Exit(1) + } + + fn := func() error { + var b [8]byte + binary.LittleEndian.PutUint64(b[:], uint64(cmd.Process.Pid)) + _, err := controlW.Write(b[:]) + if err != nil { + return fmt.Errorf("error writing child pid: %w", err) + } + + // Wait for stop. + var status syscall.WaitStatus + var errno syscall.Errno + for { + _, _, errno = syscall.Syscall6(syscall.SYS_WAIT4, uintptr(cmd.Process.Pid), uintptr(unsafe.Pointer(&status)), syscall.WUNTRACED, 0, 0, 0) + if errno != syscall.EINTR { + break + } + } + if errno != 0 { + return fmt.Errorf("error waiting for stop: %w", errno) + } + + if !status.Stopped() { + return fmt.Errorf("unexpected wait status: %v", status) + } + + // Take TTY. + pgrp := int32(syscall.Getpgrp()) // assume that pid_t is int32 + _, _, errno = syscall.Syscall(syscall.SYS_IOCTL, ptyFD, syscall.TIOCSPGRP, uintptr(unsafe.Pointer(&pgrp))) + if errno != 0 { + return fmt.Errorf("error setting tty process group: %w", errno) + } + + // Give the kernel time to potentially wake readers and have + // them return EINTR (darwin does this). + time.Sleep(pause) + + // Give TTY back. + pid := int32(cmd.Process.Pid) // assume that pid_t is int32 + _, _, errno = syscall.Syscall(syscall.SYS_IOCTL, ptyFD, syscall.TIOCSPGRP, uintptr(unsafe.Pointer(&pid))) + if errno != 0 { + return fmt.Errorf("error setting tty process group back: %w", errno) + } + + // Report that we are done and SIGCONT can be sent. Note that + // the actual byte we send doesn't matter. + if _, err := controlW.Write(b[:1]); err != nil { + return fmt.Errorf("error writing readiness: %w", err) + } + + return nil + } + + err := fn() + if err != nil { + fmt.Fprintf(os.Stderr, "session leader error: %v\n", err) + cmd.Process.Kill() + // Wait for exit below. + } + + werr := cmd.Wait() + if werr != nil { + fmt.Fprintf(os.Stderr, "error running second subprocess: %v\n", err) + } + + if err != nil || werr != nil { + os.Exit(1) + } + + os.Exit(0) +} + +// GO_TEST_TERMINAL_SIGNALS=2 subprocess above. +func runStoppingChild() { + pty := os.NewFile(ptyFD, "pty") + + var b [1]byte + if _, err := pty.Write(b[:]); err != nil { + fmt.Fprintf(os.Stderr, "error writing byte to PTY: %v\n", err) + os.Exit(1) + } + + _, err := pty.Read(b[:]) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if b[0] == '\n' { + // This is what we expect + fmt.Println("read newline") + } else { + fmt.Fprintf(os.Stderr, "read 1 unexpected byte: %q\n", b) + os.Exit(1) + } + os.Exit(0) +} diff --git a/src/os/signal/signal_linux_test.go b/src/os/signal/signal_linux_test.go new file mode 100644 index 0000000..f70f108 --- /dev/null +++ b/src/os/signal/signal_linux_test.go @@ -0,0 +1,42 @@ +// 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. + +//go:build linux + +package signal + +import ( + "os" + "syscall" + "testing" + "time" +) + +const prSetKeepCaps = 8 + +// This test validates that syscall.AllThreadsSyscall() can reliably +// reach all 'm' (threads) of the nocgo runtime even when one thread +// is blocked waiting to receive signals from the kernel. This monitors +// for a regression vs. the fix for #43149. +func TestAllThreadsSyscallSignals(t *testing.T) { + if _, _, err := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, prSetKeepCaps, 0, 0); err == syscall.ENOTSUP { + t.Skip("AllThreadsSyscall disabled with cgo") + } + + sig := make(chan os.Signal, 1) + Notify(sig, os.Interrupt) + + for i := 0; i <= 100; i++ { + if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, prSetKeepCaps, uintptr(i&1), 0); errno != 0 { + t.Fatalf("[%d] failed to set KEEP_CAPS=%d: %v", i, i&1, errno) + } + } + + select { + case <-time.After(10 * time.Millisecond): + case <-sig: + t.Fatal("unexpected signal") + } + Stop(sig) +} diff --git a/src/os/signal/signal_plan9.go b/src/os/signal/signal_plan9.go new file mode 100644 index 0000000..7d48715 --- /dev/null +++ b/src/os/signal/signal_plan9.go @@ -0,0 +1,64 @@ +// Copyright 2012 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 signal + +import ( + "os" + "syscall" +) + +var sigtab = make(map[os.Signal]int) + +// Defined by the runtime package. +func signal_disable(uint32) +func signal_enable(uint32) +func signal_ignore(uint32) +func signal_ignored(uint32) bool +func signal_recv() string + +func init() { + watchSignalLoop = loop +} + +func loop() { + for { + process(syscall.Note(signal_recv())) + } +} + +const numSig = 256 + +func signum(sig os.Signal) int { + switch sig := sig.(type) { + case syscall.Note: + n, ok := sigtab[sig] + if !ok { + n = len(sigtab) + 1 + if n > numSig { + return -1 + } + sigtab[sig] = n + } + return n + default: + return -1 + } +} + +func enableSignal(sig int) { + signal_enable(uint32(sig)) +} + +func disableSignal(sig int) { + signal_disable(uint32(sig)) +} + +func ignoreSignal(sig int) { + signal_ignore(uint32(sig)) +} + +func signalIgnored(sig int) bool { + return signal_ignored(uint32(sig)) +} diff --git a/src/os/signal/signal_plan9_test.go b/src/os/signal/signal_plan9_test.go new file mode 100644 index 0000000..8357199 --- /dev/null +++ b/src/os/signal/signal_plan9_test.go @@ -0,0 +1,167 @@ +// Copyright 2009 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 signal + +import ( + "internal/itoa" + "os" + "runtime" + "syscall" + "testing" + "time" +) + +func waitSig(t *testing.T, c <-chan os.Signal, sig os.Signal) { + select { + case s := <-c: + if s != sig { + t.Fatalf("signal was %v, want %v", s, sig) + } + case <-time.After(1 * time.Second): + t.Fatalf("timeout waiting for %v", sig) + } +} + +// Test that basic signal handling works. +func TestSignal(t *testing.T) { + // Ask for hangup + c := make(chan os.Signal, 1) + Notify(c, syscall.Note("hangup")) + defer Stop(c) + + // Send this process a hangup + t.Logf("hangup...") + postNote(syscall.Getpid(), "hangup") + waitSig(t, c, syscall.Note("hangup")) + + // Ask for everything we can get. + c1 := make(chan os.Signal, 1) + Notify(c1) + + // Send this process an alarm + t.Logf("alarm...") + postNote(syscall.Getpid(), "alarm") + waitSig(t, c1, syscall.Note("alarm")) + + // Send two more hangups, to make sure that + // they get delivered on c1 and that not reading + // from c does not block everything. + t.Logf("hangup...") + postNote(syscall.Getpid(), "hangup") + waitSig(t, c1, syscall.Note("hangup")) + t.Logf("hangup...") + postNote(syscall.Getpid(), "hangup") + waitSig(t, c1, syscall.Note("hangup")) + + // The first SIGHUP should be waiting for us on c. + waitSig(t, c, syscall.Note("hangup")) +} + +func TestStress(t *testing.T) { + dur := 3 * time.Second + if testing.Short() { + dur = 100 * time.Millisecond + } + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4)) + done := make(chan bool) + finished := make(chan bool) + go func() { + sig := make(chan os.Signal, 1) + Notify(sig, syscall.Note("alarm")) + defer Stop(sig) + Loop: + for { + select { + case <-sig: + case <-done: + break Loop + } + } + finished <- true + }() + go func() { + Loop: + for { + select { + case <-done: + break Loop + default: + postNote(syscall.Getpid(), "alarm") + runtime.Gosched() + } + } + finished <- true + }() + time.Sleep(dur) + close(done) + <-finished + <-finished + // When run with 'go test -cpu=1,2,4' alarm from this test can slip + // into subsequent TestSignal() causing failure. + // Sleep for a while to reduce the possibility of the failure. + time.Sleep(10 * time.Millisecond) +} + +// Test that Stop cancels the channel's registrations. +func TestStop(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + sigs := []string{ + "alarm", + "hangup", + } + + for _, sig := range sigs { + // Send the signal. + // If it's alarm, we should not see it. + // If it's hangup, maybe we'll die. Let the flag tell us what to do. + if sig != "hangup" { + postNote(syscall.Getpid(), sig) + } + time.Sleep(100 * time.Millisecond) + + // Ask for signal + c := make(chan os.Signal, 1) + Notify(c, syscall.Note(sig)) + defer Stop(c) + + // Send this process that signal + postNote(syscall.Getpid(), sig) + waitSig(t, c, syscall.Note(sig)) + + Stop(c) + select { + case s := <-c: + t.Fatalf("unexpected signal %v", s) + case <-time.After(100 * time.Millisecond): + // nothing to read - good + } + + // Send the signal. + // If it's alarm, we should not see it. + // If it's hangup, maybe we'll die. Let the flag tell us what to do. + if sig != "hangup" { + postNote(syscall.Getpid(), sig) + } + + select { + case s := <-c: + t.Fatalf("unexpected signal %v", s) + case <-time.After(100 * time.Millisecond): + // nothing to read - good + } + } +} + +func postNote(pid int, note string) error { + f, err := os.OpenFile("/proc/"+itoa.Itoa(pid)+"/note", os.O_WRONLY, 0) + if err != nil { + return err + } + defer f.Close() + _, err = f.Write([]byte(note)) + return err +} diff --git a/src/os/signal/signal_test.go b/src/os/signal/signal_test.go new file mode 100644 index 0000000..c7c42ed --- /dev/null +++ b/src/os/signal/signal_test.go @@ -0,0 +1,932 @@ +// Copyright 2009 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. + +//go:build unix + +package signal + +import ( + "bytes" + "context" + "flag" + "fmt" + "internal/testenv" + "os" + "os/exec" + "runtime" + "runtime/trace" + "strconv" + "strings" + "sync" + "syscall" + "testing" + "time" +) + +// settleTime is an upper bound on how long we expect signals to take to be +// delivered. Lower values make the test faster, but also flakier — especially +// on heavily loaded systems. +// +// The current value is set based on flakes observed in the Go builders. +var settleTime = 100 * time.Millisecond + +// fatalWaitingTime is an absurdly long time to wait for signals to be +// delivered but, using it, we (hopefully) eliminate test flakes on the +// build servers. See #46736 for discussion. +var fatalWaitingTime = 30 * time.Second + +func init() { + if testenv.Builder() == "solaris-amd64-oraclerel" { + // The solaris-amd64-oraclerel builder has been observed to time out in + // TestNohup even with a 250ms settle time. + // + // Use a much longer settle time on that builder to try to suss out whether + // the test is flaky due to builder slowness (which may mean we need a + // longer GO_TEST_TIMEOUT_SCALE) or due to a dropped signal (which may + // instead need a test-skip and upstream bug filed against the Solaris + // kernel). + // + // See https://golang.org/issue/33174. + settleTime = 5 * time.Second + } else if runtime.GOOS == "linux" && strings.HasPrefix(runtime.GOARCH, "ppc64") { + // Older linux kernels seem to have some hiccups delivering the signal + // in a timely manner on ppc64 and ppc64le. When running on a + // ppc64le/ubuntu 16.04/linux 4.4 host the time can vary quite + // substantially even on a idle system. 5 seconds is twice any value + // observed when running 10000 tests on such a system. + settleTime = 5 * time.Second + } else if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { + if scale, err := strconv.Atoi(s); err == nil { + settleTime *= time.Duration(scale) + } + } +} + +func waitSig(t *testing.T, c <-chan os.Signal, sig os.Signal) { + t.Helper() + waitSig1(t, c, sig, false) +} +func waitSigAll(t *testing.T, c <-chan os.Signal, sig os.Signal) { + t.Helper() + waitSig1(t, c, sig, true) +} + +func waitSig1(t *testing.T, c <-chan os.Signal, sig os.Signal, all bool) { + t.Helper() + + // Sleep multiple times to give the kernel more tries to + // deliver the signal. + start := time.Now() + timer := time.NewTimer(settleTime / 10) + defer timer.Stop() + // If the caller notified for all signals on c, filter out SIGURG, + // which is used for runtime preemption and can come at unpredictable times. + // General user code should filter out all unexpected signals instead of just + // SIGURG, but since os/signal is tightly coupled to the runtime it seems + // appropriate to be stricter here. + for time.Since(start) < fatalWaitingTime { + select { + case s := <-c: + if s == sig { + return + } + if !all || s != syscall.SIGURG { + t.Fatalf("signal was %v, want %v", s, sig) + } + case <-timer.C: + timer.Reset(settleTime / 10) + } + } + t.Fatalf("timeout after %v waiting for %v", fatalWaitingTime, sig) +} + +// quiesce waits until we can be reasonably confident that all pending signals +// have been delivered by the OS. +func quiesce() { + // The kernel will deliver a signal as a thread returns + // from a syscall. If the only active thread is sleeping, + // and the system is busy, the kernel may not get around + // to waking up a thread to catch the signal. + // We try splitting up the sleep to give the kernel + // many chances to deliver the signal. + start := time.Now() + for time.Since(start) < settleTime { + time.Sleep(settleTime / 10) + } +} + +// Test that basic signal handling works. +func TestSignal(t *testing.T) { + // Ask for SIGHUP + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGHUP) + defer Stop(c) + + // Send this process a SIGHUP + t.Logf("sighup...") + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + waitSig(t, c, syscall.SIGHUP) + + // Ask for everything we can get. The buffer size has to be + // more than 1, since the runtime might send SIGURG signals. + // Using 10 is arbitrary. + c1 := make(chan os.Signal, 10) + Notify(c1) + // Stop relaying the SIGURG signals. See #49724 + Reset(syscall.SIGURG) + defer Stop(c1) + + // Send this process a SIGWINCH + t.Logf("sigwinch...") + syscall.Kill(syscall.Getpid(), syscall.SIGWINCH) + waitSigAll(t, c1, syscall.SIGWINCH) + + // Send two more SIGHUPs, to make sure that + // they get delivered on c1 and that not reading + // from c does not block everything. + t.Logf("sighup...") + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + waitSigAll(t, c1, syscall.SIGHUP) + t.Logf("sighup...") + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + waitSigAll(t, c1, syscall.SIGHUP) + + // The first SIGHUP should be waiting for us on c. + waitSig(t, c, syscall.SIGHUP) +} + +func TestStress(t *testing.T) { + dur := 3 * time.Second + if testing.Short() { + dur = 100 * time.Millisecond + } + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4)) + + sig := make(chan os.Signal, 1) + Notify(sig, syscall.SIGUSR1) + + go func() { + stop := time.After(dur) + for { + select { + case <-stop: + // Allow enough time for all signals to be delivered before we stop + // listening for them. + quiesce() + Stop(sig) + // According to its documentation, “[w]hen Stop returns, it in + // guaranteed that c will receive no more signals.” So we can safely + // close sig here: if there is a send-after-close race here, that is a + // bug in Stop and we would like to detect it. + close(sig) + return + + default: + syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) + runtime.Gosched() + } + } + }() + + for range sig { + // Receive signals until the sender closes sig. + } +} + +func testCancel(t *testing.T, ignore bool) { + // Ask to be notified on c1 when a SIGWINCH is received. + c1 := make(chan os.Signal, 1) + Notify(c1, syscall.SIGWINCH) + defer Stop(c1) + + // Ask to be notified on c2 when a SIGHUP is received. + c2 := make(chan os.Signal, 1) + Notify(c2, syscall.SIGHUP) + defer Stop(c2) + + // Send this process a SIGWINCH and wait for notification on c1. + syscall.Kill(syscall.Getpid(), syscall.SIGWINCH) + waitSig(t, c1, syscall.SIGWINCH) + + // Send this process a SIGHUP and wait for notification on c2. + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + waitSig(t, c2, syscall.SIGHUP) + + // Ignore, or reset the signal handlers for, SIGWINCH and SIGHUP. + // Either way, this should undo both calls to Notify above. + if ignore { + Ignore(syscall.SIGWINCH, syscall.SIGHUP) + // Don't bother deferring a call to Reset: it is documented to undo Notify, + // but its documentation says nothing about Ignore, and (as of the time of + // writing) it empirically does not undo an Ignore. + } else { + Reset(syscall.SIGWINCH, syscall.SIGHUP) + } + + // Send this process a SIGWINCH. It should be ignored. + syscall.Kill(syscall.Getpid(), syscall.SIGWINCH) + + // If ignoring, Send this process a SIGHUP. It should be ignored. + if ignore { + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + } + + quiesce() + + select { + case s := <-c1: + t.Errorf("unexpected signal %v", s) + default: + // nothing to read - good + } + + select { + case s := <-c2: + t.Errorf("unexpected signal %v", s) + default: + // nothing to read - good + } + + // One or both of the signals may have been blocked for this process + // by the calling process. + // Discard any queued signals now to avoid interfering with other tests. + Notify(c1, syscall.SIGWINCH) + Notify(c2, syscall.SIGHUP) + quiesce() +} + +// Test that Reset cancels registration for listed signals on all channels. +func TestReset(t *testing.T) { + testCancel(t, false) +} + +// Test that Ignore cancels registration for listed signals on all channels. +func TestIgnore(t *testing.T) { + testCancel(t, true) +} + +// Test that Ignored correctly detects changes to the ignored status of a signal. +func TestIgnored(t *testing.T) { + // Ask to be notified on SIGWINCH. + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGWINCH) + + // If we're being notified, then the signal should not be ignored. + if Ignored(syscall.SIGWINCH) { + t.Errorf("expected SIGWINCH to not be ignored.") + } + Stop(c) + Ignore(syscall.SIGWINCH) + + // We're no longer paying attention to this signal. + if !Ignored(syscall.SIGWINCH) { + t.Errorf("expected SIGWINCH to be ignored when explicitly ignoring it.") + } + + Reset() +} + +var checkSighupIgnored = flag.Bool("check_sighup_ignored", false, "if true, TestDetectNohup will fail if SIGHUP is not ignored.") + +// Test that Ignored(SIGHUP) correctly detects whether it is being run under nohup. +func TestDetectNohup(t *testing.T) { + if *checkSighupIgnored { + if !Ignored(syscall.SIGHUP) { + t.Fatal("SIGHUP is not ignored.") + } else { + t.Log("SIGHUP is ignored.") + } + } else { + defer Reset() + // Ugly: ask for SIGHUP so that child will not have no-hup set + // even if test is running under nohup environment. + // We have no intention of reading from c. + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGHUP) + if out, err := testenv.Command(t, os.Args[0], "-test.run=TestDetectNohup", "-check_sighup_ignored").CombinedOutput(); err == nil { + t.Errorf("ran test with -check_sighup_ignored and it succeeded: expected failure.\nOutput:\n%s", out) + } + Stop(c) + + // Again, this time with nohup, assuming we can find it. + _, err := os.Stat("/usr/bin/nohup") + if err != nil { + t.Skip("cannot find nohup; skipping second half of test") + } + Ignore(syscall.SIGHUP) + os.Remove("nohup.out") + out, err := testenv.Command(t, "/usr/bin/nohup", os.Args[0], "-test.run=TestDetectNohup", "-check_sighup_ignored").CombinedOutput() + + data, _ := os.ReadFile("nohup.out") + os.Remove("nohup.out") + if err != nil { + // nohup doesn't work on new LUCI darwin builders due to the + // type of launchd service the test run under. See + // https://go.dev/issue/63875. + if runtime.GOOS == "darwin" && strings.Contains(string(out), "nohup: can't detach from console: Inappropriate ioctl for device") { + t.Skip("Skipping nohup test due to darwin builder limitation. See https://go.dev/issue/63875.") + } + + t.Errorf("ran test with -check_sighup_ignored under nohup and it failed: expected success.\nError: %v\nOutput:\n%s%s", err, out, data) + } + } +} + +var ( + sendUncaughtSighup = flag.Int("send_uncaught_sighup", 0, "send uncaught SIGHUP during TestStop") + dieFromSighup = flag.Bool("die_from_sighup", false, "wait to die from uncaught SIGHUP") +) + +// Test that Stop cancels the channel's registrations. +func TestStop(t *testing.T) { + sigs := []syscall.Signal{ + syscall.SIGWINCH, + syscall.SIGHUP, + syscall.SIGUSR1, + } + + for _, sig := range sigs { + sig := sig + t.Run(fmt.Sprint(sig), func(t *testing.T) { + // When calling Notify with a specific signal, + // independent signals should not interfere with each other, + // and we end up needing to wait for signals to quiesce a lot. + // Test the three different signals concurrently. + t.Parallel() + + // If the signal is not ignored, send the signal before registering a + // channel to verify the behavior of the default Go handler. + // If it's SIGWINCH or SIGUSR1 we should not see it. + // If it's SIGHUP, maybe we'll die. Let the flag tell us what to do. + mayHaveBlockedSignal := false + if !Ignored(sig) && (sig != syscall.SIGHUP || *sendUncaughtSighup == 1) { + syscall.Kill(syscall.Getpid(), sig) + quiesce() + + // We don't know whether sig is blocked for this process; see + // https://golang.org/issue/38165. Assume that it could be. + mayHaveBlockedSignal = true + } + + // Ask for signal + c := make(chan os.Signal, 1) + Notify(c, sig) + + // Send this process the signal again. + syscall.Kill(syscall.Getpid(), sig) + waitSig(t, c, sig) + + if mayHaveBlockedSignal { + // We may have received a queued initial signal in addition to the one + // that we sent after Notify. If so, waitSig may have observed that + // initial signal instead of the second one, and we may need to wait for + // the second signal to clear. Do that now. + quiesce() + select { + case <-c: + default: + } + } + + // Stop watching for the signal and send it again. + // If it's SIGHUP, maybe we'll die. Let the flag tell us what to do. + Stop(c) + if sig != syscall.SIGHUP || *sendUncaughtSighup == 2 { + syscall.Kill(syscall.Getpid(), sig) + quiesce() + + select { + case s := <-c: + t.Errorf("unexpected signal %v", s) + default: + // nothing to read - good + } + + // If we're going to receive a signal, it has almost certainly been + // received by now. However, it may have been blocked for this process — + // we don't know. Explicitly unblock it and wait for it to clear now. + Notify(c, sig) + quiesce() + Stop(c) + } + }) + } +} + +// Test that when run under nohup, an uncaught SIGHUP does not kill the program. +func TestNohup(t *testing.T) { + // When run without nohup, the test should crash on an uncaught SIGHUP. + // When run under nohup, the test should ignore uncaught SIGHUPs, + // because the runtime is not supposed to be listening for them. + // Either way, TestStop should still be able to catch them when it wants them + // and then when it stops wanting them, the original behavior should resume. + // + // send_uncaught_sighup=1 sends the SIGHUP before starting to listen for SIGHUPs. + // send_uncaught_sighup=2 sends the SIGHUP after no longer listening for SIGHUPs. + // + // Both should fail without nohup and succeed with nohup. + + t.Run("uncaught", func(t *testing.T) { + // Ugly: ask for SIGHUP so that child will not have no-hup set + // even if test is running under nohup environment. + // We have no intention of reading from c. + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGHUP) + t.Cleanup(func() { Stop(c) }) + + var subTimeout time.Duration + if deadline, ok := t.Deadline(); ok { + subTimeout = time.Until(deadline) + subTimeout -= subTimeout / 10 // Leave 10% headroom for propagating output. + } + for i := 1; i <= 2; i++ { + i := i + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() + + args := []string{ + "-test.v", + "-test.run=TestStop", + "-send_uncaught_sighup=" + strconv.Itoa(i), + "-die_from_sighup", + } + if subTimeout != 0 { + args = append(args, fmt.Sprintf("-test.timeout=%v", subTimeout)) + } + out, err := testenv.Command(t, os.Args[0], args...).CombinedOutput() + + if err == nil { + t.Errorf("ran test with -send_uncaught_sighup=%d and it succeeded: expected failure.\nOutput:\n%s", i, out) + } else { + t.Logf("test with -send_uncaught_sighup=%d failed as expected.\nError: %v\nOutput:\n%s", i, err, out) + } + }) + } + }) + + t.Run("nohup", func(t *testing.T) { + // Skip the nohup test below when running in tmux on darwin, since nohup + // doesn't work correctly there. See issue #5135. + if runtime.GOOS == "darwin" && os.Getenv("TMUX") != "" { + t.Skip("Skipping nohup test due to running in tmux on darwin") + } + + // Again, this time with nohup, assuming we can find it. + _, err := exec.LookPath("nohup") + if err != nil { + t.Skip("cannot find nohup; skipping second half of test") + } + + var subTimeout time.Duration + if deadline, ok := t.Deadline(); ok { + subTimeout = time.Until(deadline) + subTimeout -= subTimeout / 10 // Leave 10% headroom for propagating output. + } + for i := 1; i <= 2; i++ { + i := i + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() + + // POSIX specifies that nohup writes to a file named nohup.out if standard + // output is a terminal. However, for an exec.Cmd, standard output is + // not a terminal — so we don't need to read or remove that file (and, + // indeed, cannot even create it if the current user is unable to write to + // GOROOT/src, such as when GOROOT is installed and owned by root). + + args := []string{ + os.Args[0], + "-test.v", + "-test.run=TestStop", + "-send_uncaught_sighup=" + strconv.Itoa(i), + } + if subTimeout != 0 { + args = append(args, fmt.Sprintf("-test.timeout=%v", subTimeout)) + } + out, err := testenv.Command(t, "nohup", args...).CombinedOutput() + + if err != nil { + // nohup doesn't work on new LUCI darwin builders due to the + // type of launchd service the test run under. See + // https://go.dev/issue/63875. + if runtime.GOOS == "darwin" && strings.Contains(string(out), "nohup: can't detach from console: Inappropriate ioctl for device") { + // TODO(go.dev/issue/63799): A false-positive in vet reports a + // t.Skip here as invalid. Switch back to t.Skip once fixed. + t.Logf("Skipping nohup test due to darwin builder limitation. See https://go.dev/issue/63875.") + return + } + + t.Errorf("ran test with -send_uncaught_sighup=%d under nohup and it failed: expected success.\nError: %v\nOutput:\n%s", i, err, out) + } else { + t.Logf("ran test with -send_uncaught_sighup=%d under nohup.\nOutput:\n%s", i, out) + } + }) + } + }) +} + +// Test that SIGCONT works (issue 8953). +func TestSIGCONT(t *testing.T) { + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGCONT) + defer Stop(c) + syscall.Kill(syscall.Getpid(), syscall.SIGCONT) + waitSig(t, c, syscall.SIGCONT) +} + +// Test race between stopping and receiving a signal (issue 14571). +func TestAtomicStop(t *testing.T) { + if os.Getenv("GO_TEST_ATOMIC_STOP") != "" { + atomicStopTestProgram(t) + t.Fatal("atomicStopTestProgram returned") + } + + testenv.MustHaveExec(t) + + // Call Notify for SIGINT before starting the child process. + // That ensures that SIGINT is not ignored for the child. + // This is necessary because if SIGINT is ignored when a + // Go program starts, then it remains ignored, and closing + // the last notification channel for SIGINT will switch it + // back to being ignored. In that case the assumption of + // atomicStopTestProgram, that it will either die from SIGINT + // or have it be reported, breaks down, as there is a third + // option: SIGINT might be ignored. + cs := make(chan os.Signal, 1) + Notify(cs, syscall.SIGINT) + defer Stop(cs) + + const execs = 10 + for i := 0; i < execs; i++ { + timeout := "0" + if deadline, ok := t.Deadline(); ok { + timeout = time.Until(deadline).String() + } + cmd := testenv.Command(t, os.Args[0], "-test.run=TestAtomicStop", "-test.timeout="+timeout) + cmd.Env = append(os.Environ(), "GO_TEST_ATOMIC_STOP=1") + out, err := cmd.CombinedOutput() + if err == nil { + if len(out) > 0 { + t.Logf("iteration %d: output %s", i, out) + } + } else { + t.Logf("iteration %d: exit status %q: output: %s", i, err, out) + } + + lost := bytes.Contains(out, []byte("lost signal")) + if lost { + t.Errorf("iteration %d: lost signal", i) + } + + // The program should either die due to SIGINT, + // or exit with success without printing "lost signal". + if err == nil { + if len(out) > 0 && !lost { + t.Errorf("iteration %d: unexpected output", i) + } + } else { + if ee, ok := err.(*exec.ExitError); !ok { + t.Errorf("iteration %d: error (%v) has type %T; expected exec.ExitError", i, err, err) + } else if ws, ok := ee.Sys().(syscall.WaitStatus); !ok { + t.Errorf("iteration %d: error.Sys (%v) has type %T; expected syscall.WaitStatus", i, ee.Sys(), ee.Sys()) + } else if !ws.Signaled() || ws.Signal() != syscall.SIGINT { + t.Errorf("iteration %d: got exit status %v; expected SIGINT", i, ee) + } + } + } +} + +// atomicStopTestProgram is run in a subprocess by TestAtomicStop. +// It tries to trigger a signal delivery race. This function should +// either catch a signal or die from it. +func atomicStopTestProgram(t *testing.T) { + // This test won't work if SIGINT is ignored here. + if Ignored(syscall.SIGINT) { + fmt.Println("SIGINT is ignored") + os.Exit(1) + } + + const tries = 10 + + timeout := 2 * time.Second + if deadline, ok := t.Deadline(); ok { + // Give each try an equal slice of the deadline, with one slice to spare for + // cleanup. + timeout = time.Until(deadline) / (tries + 1) + } + + pid := syscall.Getpid() + printed := false + for i := 0; i < tries; i++ { + cs := make(chan os.Signal, 1) + Notify(cs, syscall.SIGINT) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + Stop(cs) + }() + + syscall.Kill(pid, syscall.SIGINT) + + // At this point we should either die from SIGINT or + // get a notification on cs. If neither happens, we + // dropped the signal. It is given 2 seconds to + // deliver, as needed for gccgo on some loaded test systems. + + select { + case <-cs: + case <-time.After(timeout): + if !printed { + fmt.Print("lost signal on tries:") + printed = true + } + fmt.Printf(" %d", i) + } + + wg.Wait() + } + if printed { + fmt.Print("\n") + } + + os.Exit(0) +} + +func TestTime(t *testing.T) { + // Test that signal works fine when we are in a call to get time, + // which on some platforms is using VDSO. See issue #34391. + dur := 3 * time.Second + if testing.Short() { + dur = 100 * time.Millisecond + } + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4)) + + sig := make(chan os.Signal, 1) + Notify(sig, syscall.SIGUSR1) + + stop := make(chan struct{}) + go func() { + for { + select { + case <-stop: + // Allow enough time for all signals to be delivered before we stop + // listening for them. + quiesce() + Stop(sig) + // According to its documentation, “[w]hen Stop returns, it in + // guaranteed that c will receive no more signals.” So we can safely + // close sig here: if there is a send-after-close race, that is a bug in + // Stop and we would like to detect it. + close(sig) + return + + default: + syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) + runtime.Gosched() + } + } + }() + + done := make(chan struct{}) + go func() { + for range sig { + // Receive signals until the sender closes sig. + } + close(done) + }() + + t0 := time.Now() + for t1 := t0; t1.Sub(t0) < dur; t1 = time.Now() { + } // hammering on getting time + + close(stop) + <-done +} + +var ( + checkNotifyContext = flag.Bool("check_notify_ctx", false, "if true, TestNotifyContext will fail if SIGINT is not received.") + ctxNotifyTimes = flag.Int("ctx_notify_times", 1, "number of times a SIGINT signal should be received") +) + +func TestNotifyContextNotifications(t *testing.T) { + if *checkNotifyContext { + ctx, _ := NotifyContext(context.Background(), syscall.SIGINT) + // We want to make sure not to be calling Stop() internally on NotifyContext() when processing a received signal. + // Being able to wait for a number of received system signals allows us to do so. + var wg sync.WaitGroup + n := *ctxNotifyTimes + wg.Add(n) + for i := 0; i < n; i++ { + go func() { + syscall.Kill(syscall.Getpid(), syscall.SIGINT) + wg.Done() + }() + } + wg.Wait() + <-ctx.Done() + fmt.Println("received SIGINT") + // Sleep to give time to simultaneous signals to reach the process. + // These signals must be ignored given stop() is not called on this code. + // We want to guarantee a SIGINT doesn't cause a premature termination of the program. + time.Sleep(settleTime) + return + } + + t.Parallel() + testCases := []struct { + name string + n int // number of times a SIGINT should be notified. + }{ + {"once", 1}, + {"multiple", 10}, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var subTimeout time.Duration + if deadline, ok := t.Deadline(); ok { + timeout := time.Until(deadline) + if timeout < 2*settleTime { + t.Fatalf("starting test with less than %v remaining", 2*settleTime) + } + subTimeout = timeout - (timeout / 10) // Leave 10% headroom for cleaning up subprocess. + } + + args := []string{ + "-test.v", + "-test.run=TestNotifyContextNotifications$", + "-check_notify_ctx", + fmt.Sprintf("-ctx_notify_times=%d", tc.n), + } + if subTimeout != 0 { + args = append(args, fmt.Sprintf("-test.timeout=%v", subTimeout)) + } + out, err := testenv.Command(t, os.Args[0], args...).CombinedOutput() + if err != nil { + t.Errorf("ran test with -check_notify_ctx_notification and it failed with %v.\nOutput:\n%s", err, out) + } + if want := []byte("received SIGINT\n"); !bytes.Contains(out, want) { + t.Errorf("got %q, wanted %q", out, want) + } + }) + } +} + +func TestNotifyContextStop(t *testing.T) { + Ignore(syscall.SIGHUP) + if !Ignored(syscall.SIGHUP) { + t.Errorf("expected SIGHUP to be ignored when explicitly ignoring it.") + } + + parent, cancelParent := context.WithCancel(context.Background()) + defer cancelParent() + c, stop := NotifyContext(parent, syscall.SIGHUP) + defer stop() + + // If we're being notified, then the signal should not be ignored. + if Ignored(syscall.SIGHUP) { + t.Errorf("expected SIGHUP to not be ignored.") + } + + if want, got := "signal.NotifyContext(context.Background.WithCancel, [hangup])", fmt.Sprint(c); want != got { + t.Errorf("c.String() = %q, wanted %q", got, want) + } + + stop() + select { + case <-c.Done(): + if got := c.Err(); got != context.Canceled { + t.Errorf("c.Err() = %q, want %q", got, context.Canceled) + } + case <-time.After(time.Second): + t.Errorf("timed out waiting for context to be done after calling stop") + } +} + +func TestNotifyContextCancelParent(t *testing.T) { + parent, cancelParent := context.WithCancel(context.Background()) + defer cancelParent() + c, stop := NotifyContext(parent, syscall.SIGINT) + defer stop() + + if want, got := "signal.NotifyContext(context.Background.WithCancel, [interrupt])", fmt.Sprint(c); want != got { + t.Errorf("c.String() = %q, want %q", got, want) + } + + cancelParent() + select { + case <-c.Done(): + if got := c.Err(); got != context.Canceled { + t.Errorf("c.Err() = %q, want %q", got, context.Canceled) + } + case <-time.After(time.Second): + t.Errorf("timed out waiting for parent context to be canceled") + } +} + +func TestNotifyContextPrematureCancelParent(t *testing.T) { + parent, cancelParent := context.WithCancel(context.Background()) + defer cancelParent() + + cancelParent() // Prematurely cancel context before calling NotifyContext. + c, stop := NotifyContext(parent, syscall.SIGINT) + defer stop() + + if want, got := "signal.NotifyContext(context.Background.WithCancel, [interrupt])", fmt.Sprint(c); want != got { + t.Errorf("c.String() = %q, want %q", got, want) + } + + select { + case <-c.Done(): + if got := c.Err(); got != context.Canceled { + t.Errorf("c.Err() = %q, want %q", got, context.Canceled) + } + case <-time.After(time.Second): + t.Errorf("timed out waiting for parent context to be canceled") + } +} + +func TestNotifyContextSimultaneousStop(t *testing.T) { + c, stop := NotifyContext(context.Background(), syscall.SIGINT) + defer stop() + + if want, got := "signal.NotifyContext(context.Background, [interrupt])", fmt.Sprint(c); want != got { + t.Errorf("c.String() = %q, want %q", got, want) + } + + var wg sync.WaitGroup + n := 10 + wg.Add(n) + for i := 0; i < n; i++ { + go func() { + stop() + wg.Done() + }() + } + wg.Wait() + select { + case <-c.Done(): + if got := c.Err(); got != context.Canceled { + t.Errorf("c.Err() = %q, want %q", got, context.Canceled) + } + case <-time.After(time.Second): + t.Errorf("expected context to be canceled") + } +} + +func TestNotifyContextStringer(t *testing.T) { + parent, cancelParent := context.WithCancel(context.Background()) + defer cancelParent() + c, stop := NotifyContext(parent, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) + defer stop() + + want := `signal.NotifyContext(context.Background.WithCancel, [hangup interrupt terminated])` + if got := fmt.Sprint(c); got != want { + t.Errorf("c.String() = %q, want %q", got, want) + } +} + +// #44193 test signal handling while stopping and starting the world. +func TestSignalTrace(t *testing.T) { + done := make(chan struct{}) + quit := make(chan struct{}) + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGHUP) + + // Source and sink for signals busy loop unsynchronized with + // trace starts and stops. We are ultimately validating that + // signals and runtime.(stop|start)TheWorldGC are compatible. + go func() { + defer close(done) + defer Stop(c) + pid := syscall.Getpid() + for { + select { + case <-quit: + return + default: + syscall.Kill(pid, syscall.SIGHUP) + } + waitSig(t, c, syscall.SIGHUP) + } + }() + + for i := 0; i < 100; i++ { + buf := new(bytes.Buffer) + if err := trace.Start(buf); err != nil { + t.Fatalf("[%d] failed to start tracing: %v", i, err) + } + time.After(1 * time.Microsecond) + trace.Stop() + size := buf.Len() + if size == 0 { + t.Fatalf("[%d] trace is empty", i) + } + } + close(quit) + <-done +} diff --git a/src/os/signal/signal_unix.go b/src/os/signal/signal_unix.go new file mode 100644 index 0000000..21dfa41 --- /dev/null +++ b/src/os/signal/signal_unix.go @@ -0,0 +1,62 @@ +// Copyright 2012 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. + +//go:build unix || (js && wasm) || wasip1 || windows + +package signal + +import ( + "os" + "syscall" +) + +// Defined by the runtime package. +func signal_disable(uint32) +func signal_enable(uint32) +func signal_ignore(uint32) +func signal_ignored(uint32) bool +func signal_recv() uint32 + +func loop() { + for { + process(syscall.Signal(signal_recv())) + } +} + +func init() { + watchSignalLoop = loop +} + +const ( + numSig = 65 // max across all systems +) + +func signum(sig os.Signal) int { + switch sig := sig.(type) { + case syscall.Signal: + i := int(sig) + if i < 0 || i >= numSig { + return -1 + } + return i + default: + return -1 + } +} + +func enableSignal(sig int) { + signal_enable(uint32(sig)) +} + +func disableSignal(sig int) { + signal_disable(uint32(sig)) +} + +func ignoreSignal(sig int) { + signal_ignore(uint32(sig)) +} + +func signalIgnored(sig int) bool { + return signal_ignored(uint32(sig)) +} diff --git a/src/os/signal/signal_windows_test.go b/src/os/signal/signal_windows_test.go new file mode 100644 index 0000000..145a805 --- /dev/null +++ b/src/os/signal/signal_windows_test.go @@ -0,0 +1,98 @@ +// Copyright 2012 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 signal + +import ( + "internal/testenv" + "os" + "path/filepath" + "strings" + "syscall" + "testing" + "time" +) + +func sendCtrlBreak(t *testing.T, pid int) { + d, e := syscall.LoadDLL("kernel32.dll") + if e != nil { + t.Fatalf("LoadDLL: %v\n", e) + } + p, e := d.FindProc("GenerateConsoleCtrlEvent") + if e != nil { + t.Fatalf("FindProc: %v\n", e) + } + r, _, e := p.Call(syscall.CTRL_BREAK_EVENT, uintptr(pid)) + if r == 0 { + t.Fatalf("GenerateConsoleCtrlEvent: %v\n", e) + } +} + +func TestCtrlBreak(t *testing.T) { + // create source file + const source = ` +package main + +import ( + "log" + "os" + "os/signal" + "time" +) + + +func main() { + c := make(chan os.Signal, 10) + signal.Notify(c) + select { + case s := <-c: + if s != os.Interrupt { + log.Fatalf("Wrong signal received: got %q, want %q\n", s, os.Interrupt) + } + case <-time.After(3 * time.Second): + log.Fatalf("Timeout waiting for Ctrl+Break\n") + } +} +` + tmp := t.TempDir() + + // write ctrlbreak.go + name := filepath.Join(tmp, "ctlbreak") + src := name + ".go" + f, err := os.Create(src) + if err != nil { + t.Fatalf("Failed to create %v: %v", src, err) + } + defer f.Close() + f.Write([]byte(source)) + + // compile it + exe := name + ".exe" + defer os.Remove(exe) + o, err := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", exe, src).CombinedOutput() + if err != nil { + t.Fatalf("Failed to compile: %v\n%v", err, string(o)) + } + + // run it + cmd := testenv.Command(t, exe) + var buf strings.Builder + cmd.Stdout = &buf + cmd.Stderr = &buf + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, + } + err = cmd.Start() + if err != nil { + t.Fatalf("Start failed: %v", err) + } + go func() { + time.Sleep(1 * time.Second) + sendCtrlBreak(t, cmd.Process.Pid) + }() + err = cmd.Wait() + if err != nil { + t.Fatalf("Program exited with error: %v\n%v", err, buf.String()) + } +} diff --git a/src/os/stat.go b/src/os/stat.go new file mode 100644 index 0000000..af66838 --- /dev/null +++ b/src/os/stat.go @@ -0,0 +1,23 @@ +// Copyright 2017 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 os + +import "internal/testlog" + +// Stat returns a FileInfo describing the named file. +// If there is an error, it will be of type *PathError. +func Stat(name string) (FileInfo, error) { + testlog.Stat(name) + return statNolog(name) +} + +// Lstat returns a FileInfo describing the named file. +// If the file is a symbolic link, the returned FileInfo +// describes the symbolic link. Lstat makes no attempt to follow the link. +// If there is an error, it will be of type *PathError. +func Lstat(name string) (FileInfo, error) { + testlog.Stat(name) + return lstatNolog(name) +} diff --git a/src/os/stat_aix.go b/src/os/stat_aix.go new file mode 100644 index 0000000..a37c9fd --- /dev/null +++ b/src/os/stat_aix.go @@ -0,0 +1,51 @@ +// Copyright 2018 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 os + +import ( + "syscall" + "time" +) + +func fillFileStatFromSys(fs *fileStat, name string) { + fs.name = basename(name) + fs.size = int64(fs.sys.Size) + fs.modTime = stTimespecToTime(fs.sys.Mtim) + fs.mode = FileMode(fs.sys.Mode & 0777) + switch fs.sys.Mode & syscall.S_IFMT { + case syscall.S_IFBLK: + fs.mode |= ModeDevice + case syscall.S_IFCHR: + fs.mode |= ModeDevice | ModeCharDevice + case syscall.S_IFDIR: + fs.mode |= ModeDir + case syscall.S_IFIFO: + fs.mode |= ModeNamedPipe + case syscall.S_IFLNK: + fs.mode |= ModeSymlink + case syscall.S_IFREG: + // nothing to do + case syscall.S_IFSOCK: + fs.mode |= ModeSocket + } + if fs.sys.Mode&syscall.S_ISGID != 0 { + fs.mode |= ModeSetgid + } + if fs.sys.Mode&syscall.S_ISUID != 0 { + fs.mode |= ModeSetuid + } + if fs.sys.Mode&syscall.S_ISVTX != 0 { + fs.mode |= ModeSticky + } +} + +func stTimespecToTime(ts syscall.StTimespec_t) time.Time { + return time.Unix(int64(ts.Sec), int64(ts.Nsec)) +} + +// For testing. +func atime(fi FileInfo) time.Time { + return stTimespecToTime(fi.Sys().(*syscall.Stat_t).Atim) +} diff --git a/src/os/stat_darwin.go b/src/os/stat_darwin.go new file mode 100644 index 0000000..b92ffd4 --- /dev/null +++ b/src/os/stat_darwin.go @@ -0,0 +1,47 @@ +// Copyright 2009 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 os + +import ( + "syscall" + "time" +) + +func fillFileStatFromSys(fs *fileStat, name string) { + fs.name = basename(name) + fs.size = fs.sys.Size + fs.modTime = time.Unix(fs.sys.Mtimespec.Unix()) + fs.mode = FileMode(fs.sys.Mode & 0777) + switch fs.sys.Mode & syscall.S_IFMT { + case syscall.S_IFBLK, syscall.S_IFWHT: + fs.mode |= ModeDevice + case syscall.S_IFCHR: + fs.mode |= ModeDevice | ModeCharDevice + case syscall.S_IFDIR: + fs.mode |= ModeDir + case syscall.S_IFIFO: + fs.mode |= ModeNamedPipe + case syscall.S_IFLNK: + fs.mode |= ModeSymlink + case syscall.S_IFREG: + // nothing to do + case syscall.S_IFSOCK: + fs.mode |= ModeSocket + } + if fs.sys.Mode&syscall.S_ISGID != 0 { + fs.mode |= ModeSetgid + } + if fs.sys.Mode&syscall.S_ISUID != 0 { + fs.mode |= ModeSetuid + } + if fs.sys.Mode&syscall.S_ISVTX != 0 { + fs.mode |= ModeSticky + } +} + +// For testing. +func atime(fi FileInfo) time.Time { + return time.Unix(fi.Sys().(*syscall.Stat_t).Atimespec.Unix()) +} diff --git a/src/os/stat_dragonfly.go b/src/os/stat_dragonfly.go new file mode 100644 index 0000000..316c26c --- /dev/null +++ b/src/os/stat_dragonfly.go @@ -0,0 +1,47 @@ +// Copyright 2009 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 os + +import ( + "syscall" + "time" +) + +func fillFileStatFromSys(fs *fileStat, name string) { + fs.name = basename(name) + fs.size = fs.sys.Size + fs.modTime = time.Unix(fs.sys.Mtim.Unix()) + fs.mode = FileMode(fs.sys.Mode & 0777) + switch fs.sys.Mode & syscall.S_IFMT { + case syscall.S_IFBLK: + fs.mode |= ModeDevice + case syscall.S_IFCHR: + fs.mode |= ModeDevice | ModeCharDevice + case syscall.S_IFDIR: + fs.mode |= ModeDir + case syscall.S_IFIFO: + fs.mode |= ModeNamedPipe + case syscall.S_IFLNK: + fs.mode |= ModeSymlink + case syscall.S_IFREG: + // nothing to do + case syscall.S_IFSOCK: + fs.mode |= ModeSocket + } + if fs.sys.Mode&syscall.S_ISGID != 0 { + fs.mode |= ModeSetgid + } + if fs.sys.Mode&syscall.S_ISUID != 0 { + fs.mode |= ModeSetuid + } + if fs.sys.Mode&syscall.S_ISVTX != 0 { + fs.mode |= ModeSticky + } +} + +// For testing. +func atime(fi FileInfo) time.Time { + return time.Unix(fi.Sys().(*syscall.Stat_t).Atim.Unix()) +} diff --git a/src/os/stat_freebsd.go b/src/os/stat_freebsd.go new file mode 100644 index 0000000..919ee44 --- /dev/null +++ b/src/os/stat_freebsd.go @@ -0,0 +1,47 @@ +// Copyright 2009 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 os + +import ( + "syscall" + "time" +) + +func fillFileStatFromSys(fs *fileStat, name string) { + fs.name = basename(name) + fs.size = fs.sys.Size + fs.modTime = time.Unix(fs.sys.Mtimespec.Unix()) + fs.mode = FileMode(fs.sys.Mode & 0777) + switch fs.sys.Mode & syscall.S_IFMT { + case syscall.S_IFBLK: + fs.mode |= ModeDevice + case syscall.S_IFCHR: + fs.mode |= ModeDevice | ModeCharDevice + case syscall.S_IFDIR: + fs.mode |= ModeDir + case syscall.S_IFIFO: + fs.mode |= ModeNamedPipe + case syscall.S_IFLNK: + fs.mode |= ModeSymlink + case syscall.S_IFREG: + // nothing to do + case syscall.S_IFSOCK: + fs.mode |= ModeSocket + } + if fs.sys.Mode&syscall.S_ISGID != 0 { + fs.mode |= ModeSetgid + } + if fs.sys.Mode&syscall.S_ISUID != 0 { + fs.mode |= ModeSetuid + } + if fs.sys.Mode&syscall.S_ISVTX != 0 { + fs.mode |= ModeSticky + } +} + +// For testing. +func atime(fi FileInfo) time.Time { + return time.Unix(fi.Sys().(*syscall.Stat_t).Atimespec.Unix()) +} diff --git a/src/os/stat_js.go b/src/os/stat_js.go new file mode 100644 index 0000000..a137172 --- /dev/null +++ b/src/os/stat_js.go @@ -0,0 +1,50 @@ +// Copyright 2009 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. + +//go:build js && wasm + +package os + +import ( + "syscall" + "time" +) + +func fillFileStatFromSys(fs *fileStat, name string) { + fs.name = basename(name) + fs.size = fs.sys.Size + fs.modTime = time.Unix(fs.sys.Mtime, fs.sys.MtimeNsec) + fs.mode = FileMode(fs.sys.Mode & 0777) + switch fs.sys.Mode & syscall.S_IFMT { + case syscall.S_IFBLK: + fs.mode |= ModeDevice + case syscall.S_IFCHR: + fs.mode |= ModeDevice | ModeCharDevice + case syscall.S_IFDIR: + fs.mode |= ModeDir + case syscall.S_IFIFO: + fs.mode |= ModeNamedPipe + case syscall.S_IFLNK: + fs.mode |= ModeSymlink + case syscall.S_IFREG: + // nothing to do + case syscall.S_IFSOCK: + fs.mode |= ModeSocket + } + if fs.sys.Mode&syscall.S_ISGID != 0 { + fs.mode |= ModeSetgid + } + if fs.sys.Mode&syscall.S_ISUID != 0 { + fs.mode |= ModeSetuid + } + if fs.sys.Mode&syscall.S_ISVTX != 0 { + fs.mode |= ModeSticky + } +} + +// For testing. +func atime(fi FileInfo) time.Time { + st := fi.Sys().(*syscall.Stat_t) + return time.Unix(st.Atime, st.AtimeNsec) +} diff --git a/src/os/stat_linux.go b/src/os/stat_linux.go new file mode 100644 index 0000000..316c26c --- /dev/null +++ b/src/os/stat_linux.go @@ -0,0 +1,47 @@ +// Copyright 2009 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 os + +import ( + "syscall" + "time" +) + +func fillFileStatFromSys(fs *fileStat, name string) { + fs.name = basename(name) + fs.size = fs.sys.Size + fs.modTime = time.Unix(fs.sys.Mtim.Unix()) + fs.mode = FileMode(fs.sys.Mode & 0777) + switch fs.sys.Mode & syscall.S_IFMT { + case syscall.S_IFBLK: + fs.mode |= ModeDevice + case syscall.S_IFCHR: + fs.mode |= ModeDevice | ModeCharDevice + case syscall.S_IFDIR: + fs.mode |= ModeDir + case syscall.S_IFIFO: + fs.mode |= ModeNamedPipe + case syscall.S_IFLNK: + fs.mode |= ModeSymlink + case syscall.S_IFREG: + // nothing to do + case syscall.S_IFSOCK: + fs.mode |= ModeSocket + } + if fs.sys.Mode&syscall.S_ISGID != 0 { + fs.mode |= ModeSetgid + } + if fs.sys.Mode&syscall.S_ISUID != 0 { + fs.mode |= ModeSetuid + } + if fs.sys.Mode&syscall.S_ISVTX != 0 { + fs.mode |= ModeSticky + } +} + +// For testing. +func atime(fi FileInfo) time.Time { + return time.Unix(fi.Sys().(*syscall.Stat_t).Atim.Unix()) +} diff --git a/src/os/stat_netbsd.go b/src/os/stat_netbsd.go new file mode 100644 index 0000000..919ee44 --- /dev/null +++ b/src/os/stat_netbsd.go @@ -0,0 +1,47 @@ +// Copyright 2009 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 os + +import ( + "syscall" + "time" +) + +func fillFileStatFromSys(fs *fileStat, name string) { + fs.name = basename(name) + fs.size = fs.sys.Size + fs.modTime = time.Unix(fs.sys.Mtimespec.Unix()) + fs.mode = FileMode(fs.sys.Mode & 0777) + switch fs.sys.Mode & syscall.S_IFMT { + case syscall.S_IFBLK: + fs.mode |= ModeDevice + case syscall.S_IFCHR: + fs.mode |= ModeDevice | ModeCharDevice + case syscall.S_IFDIR: + fs.mode |= ModeDir + case syscall.S_IFIFO: + fs.mode |= ModeNamedPipe + case syscall.S_IFLNK: + fs.mode |= ModeSymlink + case syscall.S_IFREG: + // nothing to do + case syscall.S_IFSOCK: + fs.mode |= ModeSocket + } + if fs.sys.Mode&syscall.S_ISGID != 0 { + fs.mode |= ModeSetgid + } + if fs.sys.Mode&syscall.S_ISUID != 0 { + fs.mode |= ModeSetuid + } + if fs.sys.Mode&syscall.S_ISVTX != 0 { + fs.mode |= ModeSticky + } +} + +// For testing. +func atime(fi FileInfo) time.Time { + return time.Unix(fi.Sys().(*syscall.Stat_t).Atimespec.Unix()) +} diff --git a/src/os/stat_openbsd.go b/src/os/stat_openbsd.go new file mode 100644 index 0000000..316c26c --- /dev/null +++ b/src/os/stat_openbsd.go @@ -0,0 +1,47 @@ +// Copyright 2009 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 os + +import ( + "syscall" + "time" +) + +func fillFileStatFromSys(fs *fileStat, name string) { + fs.name = basename(name) + fs.size = fs.sys.Size + fs.modTime = time.Unix(fs.sys.Mtim.Unix()) + fs.mode = FileMode(fs.sys.Mode & 0777) + switch fs.sys.Mode & syscall.S_IFMT { + case syscall.S_IFBLK: + fs.mode |= ModeDevice + case syscall.S_IFCHR: + fs.mode |= ModeDevice | ModeCharDevice + case syscall.S_IFDIR: + fs.mode |= ModeDir + case syscall.S_IFIFO: + fs.mode |= ModeNamedPipe + case syscall.S_IFLNK: + fs.mode |= ModeSymlink + case syscall.S_IFREG: + // nothing to do + case syscall.S_IFSOCK: + fs.mode |= ModeSocket + } + if fs.sys.Mode&syscall.S_ISGID != 0 { + fs.mode |= ModeSetgid + } + if fs.sys.Mode&syscall.S_ISUID != 0 { + fs.mode |= ModeSetuid + } + if fs.sys.Mode&syscall.S_ISVTX != 0 { + fs.mode |= ModeSticky + } +} + +// For testing. +func atime(fi FileInfo) time.Time { + return time.Unix(fi.Sys().(*syscall.Stat_t).Atim.Unix()) +} diff --git a/src/os/stat_plan9.go b/src/os/stat_plan9.go new file mode 100644 index 0000000..a5e9901 --- /dev/null +++ b/src/os/stat_plan9.go @@ -0,0 +1,114 @@ +// Copyright 2011 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 os + +import ( + "syscall" + "time" +) + +const bitSize16 = 2 + +func fileInfoFromStat(d *syscall.Dir) *fileStat { + fs := &fileStat{ + name: d.Name, + size: d.Length, + modTime: time.Unix(int64(d.Mtime), 0), + sys: d, + } + fs.mode = FileMode(d.Mode & 0777) + if d.Mode&syscall.DMDIR != 0 { + fs.mode |= ModeDir + } + if d.Mode&syscall.DMAPPEND != 0 { + fs.mode |= ModeAppend + } + if d.Mode&syscall.DMEXCL != 0 { + fs.mode |= ModeExclusive + } + if d.Mode&syscall.DMTMP != 0 { + fs.mode |= ModeTemporary + } + // Consider all files not served by #M as device files. + if d.Type != 'M' { + fs.mode |= ModeDevice + } + // Consider all files served by #c as character device files. + if d.Type == 'c' { + fs.mode |= ModeCharDevice + } + return fs +} + +// arg is an open *File or a path string. +func dirstat(arg any) (*syscall.Dir, error) { + var name string + var err error + + size := syscall.STATFIXLEN + 16*4 + + for i := 0; i < 2; i++ { + buf := make([]byte, bitSize16+size) + + var n int + switch a := arg.(type) { + case *File: + name = a.name + if err := a.incref("fstat"); err != nil { + return nil, err + } + n, err = syscall.Fstat(a.fd, buf) + a.decref() + case string: + name = a + n, err = syscall.Stat(a, buf) + default: + panic("phase error in dirstat") + } + + if n < bitSize16 { + return nil, &PathError{Op: "stat", Path: name, Err: err} + } + + // Pull the real size out of the stat message. + size = int(uint16(buf[0]) | uint16(buf[1])<<8) + + // If the stat message is larger than our buffer we will + // go around the loop and allocate one that is big enough. + if size <= n { + d, err := syscall.UnmarshalDir(buf[:n]) + if err != nil { + return nil, &PathError{Op: "stat", Path: name, Err: err} + } + return d, nil + } + + } + + if err == nil { + err = syscall.ErrBadStat + } + + return nil, &PathError{Op: "stat", Path: name, Err: err} +} + +// statNolog implements Stat for Plan 9. +func statNolog(name string) (FileInfo, error) { + d, err := dirstat(name) + if err != nil { + return nil, err + } + return fileInfoFromStat(d), nil +} + +// lstatNolog implements Lstat for Plan 9. +func lstatNolog(name string) (FileInfo, error) { + return statNolog(name) +} + +// For testing. +func atime(fi FileInfo) time.Time { + return time.Unix(int64(fi.Sys().(*syscall.Dir).Atime), 0) +} diff --git a/src/os/stat_solaris.go b/src/os/stat_solaris.go new file mode 100644 index 0000000..4e00ecb --- /dev/null +++ b/src/os/stat_solaris.go @@ -0,0 +1,57 @@ +// Copyright 2009 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 os + +import ( + "syscall" + "time" +) + +// These constants aren't in the syscall package, which is frozen. +// Values taken from golang.org/x/sys/unix. +const ( + _S_IFNAM = 0x5000 + _S_IFDOOR = 0xd000 + _S_IFPORT = 0xe000 +) + +func fillFileStatFromSys(fs *fileStat, name string) { + fs.name = basename(name) + fs.size = fs.sys.Size + fs.modTime = time.Unix(fs.sys.Mtim.Unix()) + fs.mode = FileMode(fs.sys.Mode & 0777) + switch fs.sys.Mode & syscall.S_IFMT { + case syscall.S_IFBLK: + fs.mode |= ModeDevice + case syscall.S_IFCHR: + fs.mode |= ModeDevice | ModeCharDevice + case syscall.S_IFDIR: + fs.mode |= ModeDir + case syscall.S_IFIFO: + fs.mode |= ModeNamedPipe + case syscall.S_IFLNK: + fs.mode |= ModeSymlink + case syscall.S_IFREG: + // nothing to do + case syscall.S_IFSOCK: + fs.mode |= ModeSocket + case _S_IFNAM, _S_IFDOOR, _S_IFPORT: + fs.mode |= ModeIrregular + } + if fs.sys.Mode&syscall.S_ISGID != 0 { + fs.mode |= ModeSetgid + } + if fs.sys.Mode&syscall.S_ISUID != 0 { + fs.mode |= ModeSetuid + } + if fs.sys.Mode&syscall.S_ISVTX != 0 { + fs.mode |= ModeSticky + } +} + +// For testing. +func atime(fi FileInfo) time.Time { + return time.Unix(fi.Sys().(*syscall.Stat_t).Atim.Unix()) +} diff --git a/src/os/stat_test.go b/src/os/stat_test.go new file mode 100644 index 0000000..9601969 --- /dev/null +++ b/src/os/stat_test.go @@ -0,0 +1,296 @@ +// Copyright 2018 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 os_test + +import ( + "internal/testenv" + "io/fs" + "os" + "path/filepath" + "testing" +) + +// testStatAndLstat verifies that all os.Stat, os.Lstat os.File.Stat and os.Readdir work. +func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCheck func(*testing.T, string, fs.FileInfo)) { + // test os.Stat + sfi, err := os.Stat(path) + if err != nil { + t.Error(err) + return + } + statCheck(t, path, sfi) + + // test os.Lstat + lsfi, err := os.Lstat(path) + if err != nil { + t.Error(err) + return + } + lstatCheck(t, path, lsfi) + + if isLink { + if os.SameFile(sfi, lsfi) { + t.Errorf("stat and lstat of %q should not be the same", path) + } + } else { + if !os.SameFile(sfi, lsfi) { + t.Errorf("stat and lstat of %q should be the same", path) + } + } + + // test os.File.Stat + f, err := os.Open(path) + if err != nil { + t.Error(err) + return + } + defer f.Close() + + sfi2, err := f.Stat() + if err != nil { + t.Error(err) + return + } + statCheck(t, path, sfi2) + + if !os.SameFile(sfi, sfi2) { + t.Errorf("stat of open %q file and stat of %q should be the same", path, path) + } + + if isLink { + if os.SameFile(sfi2, lsfi) { + t.Errorf("stat of opened %q file and lstat of %q should not be the same", path, path) + } + } else { + if !os.SameFile(sfi2, lsfi) { + t.Errorf("stat of opened %q file and lstat of %q should be the same", path, path) + } + } + + // test fs.FileInfo returned by os.Readdir + if len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) { + // skip os.Readdir test of directories with slash at the end + return + } + parentdir := filepath.Dir(path) + parent, err := os.Open(parentdir) + if err != nil { + t.Error(err) + return + } + defer parent.Close() + + fis, err := parent.Readdir(-1) + if err != nil { + t.Error(err) + return + } + var lsfi2 fs.FileInfo + base := filepath.Base(path) + for _, fi2 := range fis { + if fi2.Name() == base { + lsfi2 = fi2 + break + } + } + if lsfi2 == nil { + t.Errorf("failed to find %q in its parent", path) + return + } + lstatCheck(t, path, lsfi2) + + if !os.SameFile(lsfi, lsfi2) { + t.Errorf("lstat of %q file in %q directory and %q should be the same", lsfi2.Name(), parentdir, path) + } +} + +// testIsDir verifies that fi refers to directory. +func testIsDir(t *testing.T, path string, fi fs.FileInfo) { + t.Helper() + if !fi.IsDir() { + t.Errorf("%q should be a directory", path) + } + if fi.Mode()&fs.ModeSymlink != 0 { + t.Errorf("%q should not be a symlink", path) + } +} + +// testIsSymlink verifies that fi refers to symlink. +func testIsSymlink(t *testing.T, path string, fi fs.FileInfo) { + t.Helper() + if fi.IsDir() { + t.Errorf("%q should not be a directory", path) + } + if fi.Mode()&fs.ModeSymlink == 0 { + t.Errorf("%q should be a symlink", path) + } +} + +// testIsFile verifies that fi refers to file. +func testIsFile(t *testing.T, path string, fi fs.FileInfo) { + t.Helper() + if fi.IsDir() { + t.Errorf("%q should not be a directory", path) + } + if fi.Mode()&fs.ModeSymlink != 0 { + t.Errorf("%q should not be a symlink", path) + } +} + +func testDirStats(t *testing.T, path string) { + testStatAndLstat(t, path, false, testIsDir, testIsDir) +} + +func testFileStats(t *testing.T, path string) { + testStatAndLstat(t, path, false, testIsFile, testIsFile) +} + +func testSymlinkStats(t *testing.T, path string, isdir bool) { + if isdir { + testStatAndLstat(t, path, true, testIsDir, testIsSymlink) + } else { + testStatAndLstat(t, path, true, testIsFile, testIsSymlink) + } +} + +func testSymlinkSameFile(t *testing.T, path, link string) { + pathfi, err := os.Stat(path) + if err != nil { + t.Error(err) + return + } + + linkfi, err := os.Stat(link) + if err != nil { + t.Error(err) + return + } + if !os.SameFile(pathfi, linkfi) { + t.Errorf("os.Stat(%q) and os.Stat(%q) are not the same file", path, link) + } + + linkfi, err = os.Lstat(link) + if err != nil { + t.Error(err) + return + } + if os.SameFile(pathfi, linkfi) { + t.Errorf("os.Stat(%q) and os.Lstat(%q) are the same file", path, link) + } +} + +func testSymlinkSameFileOpen(t *testing.T, link string) { + f, err := os.Open(link) + if err != nil { + t.Error(err) + return + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + t.Error(err) + return + } + + fi2, err := os.Stat(link) + if err != nil { + t.Error(err) + return + } + + if !os.SameFile(fi, fi2) { + t.Errorf("os.Open(%q).Stat() and os.Stat(%q) are not the same file", link, link) + } +} + +func TestDirAndSymlinkStats(t *testing.T) { + testenv.MustHaveSymlink(t) + t.Parallel() + + tmpdir := t.TempDir() + dir := filepath.Join(tmpdir, "dir") + if err := os.Mkdir(dir, 0777); err != nil { + t.Fatal(err) + } + testDirStats(t, dir) + + dirlink := filepath.Join(tmpdir, "link") + if err := os.Symlink(dir, dirlink); err != nil { + t.Fatal(err) + } + testSymlinkStats(t, dirlink, true) + testSymlinkSameFile(t, dir, dirlink) + testSymlinkSameFileOpen(t, dirlink) + + linklink := filepath.Join(tmpdir, "linklink") + if err := os.Symlink(dirlink, linklink); err != nil { + t.Fatal(err) + } + testSymlinkStats(t, linklink, true) + testSymlinkSameFile(t, dir, linklink) + testSymlinkSameFileOpen(t, linklink) +} + +func TestFileAndSymlinkStats(t *testing.T) { + testenv.MustHaveSymlink(t) + t.Parallel() + + tmpdir := t.TempDir() + file := filepath.Join(tmpdir, "file") + if err := os.WriteFile(file, []byte(""), 0644); err != nil { + t.Fatal(err) + } + testFileStats(t, file) + + filelink := filepath.Join(tmpdir, "link") + if err := os.Symlink(file, filelink); err != nil { + t.Fatal(err) + } + testSymlinkStats(t, filelink, false) + testSymlinkSameFile(t, file, filelink) + testSymlinkSameFileOpen(t, filelink) + + linklink := filepath.Join(tmpdir, "linklink") + if err := os.Symlink(filelink, linklink); err != nil { + t.Fatal(err) + } + testSymlinkStats(t, linklink, false) + testSymlinkSameFile(t, file, linklink) + testSymlinkSameFileOpen(t, linklink) +} + +// see issue 27225 for details +func TestSymlinkWithTrailingSlash(t *testing.T) { + testenv.MustHaveSymlink(t) + t.Parallel() + + tmpdir := t.TempDir() + dir := filepath.Join(tmpdir, "dir") + if err := os.Mkdir(dir, 0777); err != nil { + t.Fatal(err) + } + dirlink := filepath.Join(tmpdir, "link") + if err := os.Symlink(dir, dirlink); err != nil { + t.Fatal(err) + } + dirlinkWithSlash := dirlink + string(os.PathSeparator) + + testDirStats(t, dirlinkWithSlash) + + fi1, err := os.Stat(dir) + if err != nil { + t.Error(err) + return + } + fi2, err := os.Stat(dirlinkWithSlash) + if err != nil { + t.Error(err) + return + } + if !os.SameFile(fi1, fi2) { + t.Errorf("os.Stat(%q) and os.Stat(%q) are not the same file", dir, dirlinkWithSlash) + } +} diff --git a/src/os/stat_unix.go b/src/os/stat_unix.go new file mode 100644 index 0000000..431df33 --- /dev/null +++ b/src/os/stat_unix.go @@ -0,0 +1,52 @@ +// Copyright 2016 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. + +//go:build unix || (js && wasm) || wasip1 + +package os + +import ( + "syscall" +) + +// Stat returns the FileInfo structure describing file. +// If there is an error, it will be of type *PathError. +func (f *File) Stat() (FileInfo, error) { + if f == nil { + return nil, ErrInvalid + } + var fs fileStat + err := f.pfd.Fstat(&fs.sys) + if err != nil { + return nil, &PathError{Op: "stat", Path: f.name, Err: err} + } + fillFileStatFromSys(&fs, f.name) + return &fs, nil +} + +// statNolog stats a file with no test logging. +func statNolog(name string) (FileInfo, error) { + var fs fileStat + err := ignoringEINTR(func() error { + return syscall.Stat(name, &fs.sys) + }) + if err != nil { + return nil, &PathError{Op: "stat", Path: name, Err: err} + } + fillFileStatFromSys(&fs, name) + return &fs, nil +} + +// lstatNolog lstats a file with no test logging. +func lstatNolog(name string) (FileInfo, error) { + var fs fileStat + err := ignoringEINTR(func() error { + return syscall.Lstat(name, &fs.sys) + }) + if err != nil { + return nil, &PathError{Op: "lstat", Path: name, Err: err} + } + fillFileStatFromSys(&fs, name) + return &fs, nil +} diff --git a/src/os/stat_wasip1.go b/src/os/stat_wasip1.go new file mode 100644 index 0000000..a4f0a20 --- /dev/null +++ b/src/os/stat_wasip1.go @@ -0,0 +1,40 @@ +// 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. + +//go:build wasip1 + +package os + +import ( + "syscall" + "time" +) + +func fillFileStatFromSys(fs *fileStat, name string) { + fs.name = basename(name) + fs.size = int64(fs.sys.Size) + fs.mode = FileMode(fs.sys.Mode) + fs.modTime = time.Unix(0, int64(fs.sys.Mtime)) + + switch fs.sys.Filetype { + case syscall.FILETYPE_BLOCK_DEVICE: + fs.mode |= ModeDevice + case syscall.FILETYPE_CHARACTER_DEVICE: + fs.mode |= ModeDevice | ModeCharDevice + case syscall.FILETYPE_DIRECTORY: + fs.mode |= ModeDir + case syscall.FILETYPE_SOCKET_DGRAM: + fs.mode |= ModeSocket + case syscall.FILETYPE_SOCKET_STREAM: + fs.mode |= ModeSocket + case syscall.FILETYPE_SYMBOLIC_LINK: + fs.mode |= ModeSymlink + } +} + +// For testing. +func atime(fi FileInfo) time.Time { + st := fi.Sys().(*syscall.Stat_t) + return time.Unix(0, int64(st.Atime)) +} diff --git a/src/os/stat_windows.go b/src/os/stat_windows.go new file mode 100644 index 0000000..033c3b9 --- /dev/null +++ b/src/os/stat_windows.go @@ -0,0 +1,136 @@ +// Copyright 2009 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 os + +import ( + "internal/syscall/windows" + "syscall" + "unsafe" +) + +// Stat returns the FileInfo structure describing file. +// If there is an error, it will be of type *PathError. +func (file *File) Stat() (FileInfo, error) { + if file == nil { + return nil, ErrInvalid + } + return statHandle(file.name, file.pfd.Sysfd) +} + +// stat implements both Stat and Lstat of a file. +func stat(funcname, name string, followSymlinks bool) (FileInfo, error) { + if len(name) == 0 { + return nil, &PathError{Op: funcname, Path: name, Err: syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)} + } + namep, err := syscall.UTF16PtrFromString(fixLongPath(name)) + if err != nil { + return nil, &PathError{Op: funcname, Path: name, Err: err} + } + + // Try GetFileAttributesEx first, because it is faster than CreateFile. + // See https://golang.org/issues/19922#issuecomment-300031421 for details. + var fa syscall.Win32FileAttributeData + err = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa))) + + // GetFileAttributesEx fails with ERROR_SHARING_VIOLATION error for + // files like c:\pagefile.sys. Use FindFirstFile for such files. + if err == windows.ERROR_SHARING_VIOLATION { + var fd syscall.Win32finddata + sh, err := syscall.FindFirstFile(namep, &fd) + if err != nil { + return nil, &PathError{Op: "FindFirstFile", Path: name, Err: err} + } + syscall.FindClose(sh) + if fd.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 { + // Not a symlink or mount point. FindFirstFile is good enough. + fs := newFileStatFromWin32finddata(&fd) + if err := fs.saveInfoFromPath(name); err != nil { + return nil, err + } + return fs, nil + } + } + + if err == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 { + // The file is definitely not a symlink, because it isn't any kind of reparse point. + // The information we got from GetFileAttributesEx is good enough for now. + fs := &fileStat{ + FileAttributes: fa.FileAttributes, + CreationTime: fa.CreationTime, + LastAccessTime: fa.LastAccessTime, + LastWriteTime: fa.LastWriteTime, + FileSizeHigh: fa.FileSizeHigh, + FileSizeLow: fa.FileSizeLow, + } + if err := fs.saveInfoFromPath(name); err != nil { + return nil, err + } + return fs, nil + } + + // Use CreateFile to determine whether the file is a symlink and, if so, + // save information about the link target. + // Set FILE_FLAG_BACKUP_SEMANTICS so that CreateFile will create the handle + // even if name refers to a directory. + h, err := syscall.CreateFile(namep, 0, 0, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OPEN_REPARSE_POINT, 0) + if err != nil { + // Since CreateFile failed, we can't determine whether name refers to a + // symlink, or some other kind of reparse point. Since we can't return a + // FileInfo with a known-accurate Mode, we must return an error. + return nil, &PathError{Op: "CreateFile", Path: name, Err: err} + } + + fi, err := statHandle(name, h) + syscall.CloseHandle(h) + if err == nil && followSymlinks && fi.(*fileStat).isSymlink() { + // To obtain information about the link target, we reopen the file without + // FILE_FLAG_OPEN_REPARSE_POINT and examine the resulting handle. + // (See https://devblogs.microsoft.com/oldnewthing/20100212-00/?p=14963.) + h, err = syscall.CreateFile(namep, 0, 0, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0) + if err != nil { + // name refers to a symlink, but we couldn't resolve the symlink target. + return nil, &PathError{Op: "CreateFile", Path: name, Err: err} + } + defer syscall.CloseHandle(h) + return statHandle(name, h) + } + return fi, err +} + +func statHandle(name string, h syscall.Handle) (FileInfo, error) { + ft, err := syscall.GetFileType(h) + if err != nil { + return nil, &PathError{Op: "GetFileType", Path: name, Err: err} + } + switch ft { + case syscall.FILE_TYPE_PIPE, syscall.FILE_TYPE_CHAR: + return &fileStat{name: basename(name), filetype: ft}, nil + } + fs, err := newFileStatFromGetFileInformationByHandle(name, h) + if err != nil { + return nil, err + } + fs.filetype = ft + return fs, err +} + +// statNolog implements Stat for Windows. +func statNolog(name string) (FileInfo, error) { + return stat("Stat", name, true) +} + +// lstatNolog implements Lstat for Windows. +func lstatNolog(name string) (FileInfo, error) { + followSymlinks := false + if name != "" && IsPathSeparator(name[len(name)-1]) { + // We try to implement POSIX semantics for Lstat path resolution + // (per https://pubs.opengroup.org/onlinepubs/9699919799.2013edition/basedefs/V1_chap04.html#tag_04_12): + // symlinks before the last separator in the path must be resolved. Since + // the last separator in this case follows the last path element, we should + // follow symlinks in the last path element. + followSymlinks = true + } + return stat("Lstat", name, followSymlinks) +} diff --git a/src/os/sticky_bsd.go b/src/os/sticky_bsd.go new file mode 100644 index 0000000..a6d9339 --- /dev/null +++ b/src/os/sticky_bsd.go @@ -0,0 +1,11 @@ +// Copyright 2014 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. + +//go:build aix || darwin || dragonfly || freebsd || (js && wasm) || netbsd || openbsd || solaris || wasip1 + +package os + +// According to sticky(8), neither open(2) nor mkdir(2) will create +// a file with the sticky bit set. +const supportsCreateWithStickyBit = false diff --git a/src/os/sticky_notbsd.go b/src/os/sticky_notbsd.go new file mode 100644 index 0000000..1d289b0 --- /dev/null +++ b/src/os/sticky_notbsd.go @@ -0,0 +1,9 @@ +// Copyright 2014 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. + +//go:build !aix && !darwin && !dragonfly && !freebsd && !js && !netbsd && !openbsd && !solaris && !wasip1 + +package os + +const supportsCreateWithStickyBit = true diff --git a/src/os/str.go b/src/os/str.go new file mode 100644 index 0000000..242c945 --- /dev/null +++ b/src/os/str.go @@ -0,0 +1,39 @@ +// Copyright 2009 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. + +// Simple conversions to avoid depending on strconv. + +package os + +// itox converts val (an int) to a hexadecimal string. +func itox(val int) string { + if val < 0 { + return "-" + uitox(uint(-val)) + } + return uitox(uint(val)) +} + +const hex = "0123456789abcdef" + +// uitox converts val (a uint) to a hexadecimal string. +func uitox(val uint) string { + if val == 0 { // avoid string allocation + return "0x0" + } + var buf [20]byte // big enough for 64bit value base 16 + 0x + i := len(buf) - 1 + for val >= 16 { + q := val / 16 + buf[i] = hex[val%16] + i-- + val = q + } + // val < 16 + buf[i] = hex[val%16] + i-- + buf[i] = 'x' + i-- + buf[i] = '0' + return string(buf[i:]) +} diff --git a/src/os/sys.go b/src/os/sys.go new file mode 100644 index 0000000..28b0f6b --- /dev/null +++ b/src/os/sys.go @@ -0,0 +1,10 @@ +// Copyright 2012 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 os + +// Hostname returns the host name reported by the kernel. +func Hostname() (name string, err error) { + return hostname() +} diff --git a/src/os/sys_aix.go b/src/os/sys_aix.go new file mode 100644 index 0000000..53a40f2 --- /dev/null +++ b/src/os/sys_aix.go @@ -0,0 +1,26 @@ +// Copyright 2018 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 os + +import "syscall" + +// gethostname syscall cannot be used because it also returns the domain. +// Therefore, hostname is retrieve with uname syscall and the Nodename field. + +func hostname() (name string, err error) { + var u syscall.Utsname + if errno := syscall.Uname(&u); errno != nil { + return "", NewSyscallError("uname", errno) + } + b := make([]byte, len(u.Nodename)) + i := 0 + for ; i < len(u.Nodename); i++ { + if u.Nodename[i] == 0 { + break + } + b[i] = byte(u.Nodename[i]) + } + return string(b[:i]), nil +} diff --git a/src/os/sys_bsd.go b/src/os/sys_bsd.go new file mode 100644 index 0000000..63120fb --- /dev/null +++ b/src/os/sys_bsd.go @@ -0,0 +1,17 @@ +// Copyright 2009 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. + +//go:build darwin || dragonfly || freebsd || (js && wasm) || netbsd || openbsd || wasip1 + +package os + +import "syscall" + +func hostname() (name string, err error) { + name, err = syscall.Sysctl("kern.hostname") + if err != nil { + return "", NewSyscallError("sysctl kern.hostname", err) + } + return name, nil +} diff --git a/src/os/sys_js.go b/src/os/sys_js.go new file mode 100644 index 0000000..4fd0e2d --- /dev/null +++ b/src/os/sys_js.go @@ -0,0 +1,11 @@ +// Copyright 2018 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. + +//go:build js && wasm + +package os + +// supportsCloseOnExec reports whether the platform supports the +// O_CLOEXEC flag. +const supportsCloseOnExec = false diff --git a/src/os/sys_linux.go b/src/os/sys_linux.go new file mode 100644 index 0000000..36a8a24 --- /dev/null +++ b/src/os/sys_linux.go @@ -0,0 +1,53 @@ +// Copyright 2009 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 os + +import ( + "runtime" + "syscall" +) + +func hostname() (name string, err error) { + // Try uname first, as it's only one system call and reading + // from /proc is not allowed on Android. + var un syscall.Utsname + err = syscall.Uname(&un) + + var buf [512]byte // Enough for a DNS name. + for i, b := range un.Nodename[:] { + buf[i] = uint8(b) + if b == 0 { + name = string(buf[:i]) + break + } + } + // If we got a name and it's not potentially truncated + // (Nodename is 65 bytes), return it. + if err == nil && len(name) > 0 && len(name) < 64 { + return name, nil + } + if runtime.GOOS == "android" { + if name != "" { + return name, nil + } + return "localhost", nil + } + + f, err := Open("/proc/sys/kernel/hostname") + if err != nil { + return "", err + } + defer f.Close() + + n, err := f.Read(buf[:]) + if err != nil { + return "", err + } + + if n > 0 && buf[n-1] == '\n' { + n-- + } + return string(buf[:n]), nil +} diff --git a/src/os/sys_plan9.go b/src/os/sys_plan9.go new file mode 100644 index 0000000..40374eb --- /dev/null +++ b/src/os/sys_plan9.go @@ -0,0 +1,24 @@ +// Copyright 2011 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 os + +func hostname() (name string, err error) { + f, err := Open("#c/sysname") + if err != nil { + return "", err + } + defer f.Close() + + var buf [128]byte + n, err := f.Read(buf[:len(buf)-1]) + + if err != nil { + return "", err + } + if n > 0 { + buf[n] = 0 + } + return string(buf[0:n]), nil +} diff --git a/src/os/sys_solaris.go b/src/os/sys_solaris.go new file mode 100644 index 0000000..917e8f2 --- /dev/null +++ b/src/os/sys_solaris.go @@ -0,0 +1,11 @@ +// Copyright 2013 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 os + +import "syscall" + +func hostname() (name string, err error) { + return syscall.Gethostname() +} diff --git a/src/os/sys_unix.go b/src/os/sys_unix.go new file mode 100644 index 0000000..79005c2 --- /dev/null +++ b/src/os/sys_unix.go @@ -0,0 +1,14 @@ +// Copyright 2014 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. + +//go:build unix + +package os + +// supportsCloseOnExec reports whether the platform supports the +// O_CLOEXEC flag. +// On Darwin, the O_CLOEXEC flag was introduced in OS X 10.7 (Darwin 11.0.0). +// See https://support.apple.com/kb/HT1633. +// On FreeBSD, the O_CLOEXEC flag was introduced in version 8.3. +const supportsCloseOnExec = true diff --git a/src/os/sys_wasip1.go b/src/os/sys_wasip1.go new file mode 100644 index 0000000..5a29aa5 --- /dev/null +++ b/src/os/sys_wasip1.go @@ -0,0 +1,11 @@ +// 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. + +//go:build wasip1 + +package os + +// supportsCloseOnExec reports whether the platform supports the +// O_CLOEXEC flag. +const supportsCloseOnExec = false diff --git a/src/os/sys_windows.go b/src/os/sys_windows.go new file mode 100644 index 0000000..72ad90b --- /dev/null +++ b/src/os/sys_windows.go @@ -0,0 +1,33 @@ +// Copyright 2009 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 os + +import ( + "internal/syscall/windows" + "syscall" +) + +func hostname() (name string, err error) { + // Use PhysicalDnsHostname to uniquely identify host in a cluster + const format = windows.ComputerNamePhysicalDnsHostname + + n := uint32(64) + for { + b := make([]uint16, n) + err := windows.GetComputerNameEx(format, &b[0], &n) + if err == nil { + return syscall.UTF16ToString(b[:n]), nil + } + if err != syscall.ERROR_MORE_DATA { + return "", NewSyscallError("ComputerNameEx", err) + } + + // If we received an ERROR_MORE_DATA, but n doesn't get larger, + // something has gone wrong and we may be in an infinite loop + if n <= uint32(len(b)) { + return "", NewSyscallError("ComputerNameEx", err) + } + } +} diff --git a/src/os/tempfile.go b/src/os/tempfile.go new file mode 100644 index 0000000..99f65c6 --- /dev/null +++ b/src/os/tempfile.go @@ -0,0 +1,128 @@ +// Copyright 2010 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 os + +import ( + "errors" + "internal/itoa" +) + +// fastrand provided by runtime. +// We generate random temporary file names so that there's a good +// chance the file doesn't exist yet - keeps the number of tries in +// TempFile to a minimum. +func fastrand() uint32 + +func nextRandom() string { + return itoa.Uitoa(uint(fastrand())) +} + +// CreateTemp creates a new temporary file in the directory dir, +// opens the file for reading and writing, and returns the resulting file. +// The filename is generated by taking pattern and adding a random string to the end. +// If pattern includes a "*", the random string replaces the last "*". +// If dir is the empty string, CreateTemp uses the default directory for temporary files, as returned by TempDir. +// Multiple programs or goroutines calling CreateTemp simultaneously will not choose the same file. +// The caller can use the file's Name method to find the pathname of the file. +// It is the caller's responsibility to remove the file when it is no longer needed. +func CreateTemp(dir, pattern string) (*File, error) { + if dir == "" { + dir = TempDir() + } + + prefix, suffix, err := prefixAndSuffix(pattern) + if err != nil { + return nil, &PathError{Op: "createtemp", Path: pattern, Err: err} + } + prefix = joinPath(dir, prefix) + + try := 0 + for { + name := prefix + nextRandom() + suffix + f, err := OpenFile(name, O_RDWR|O_CREATE|O_EXCL, 0600) + if IsExist(err) { + if try++; try < 10000 { + continue + } + return nil, &PathError{Op: "createtemp", Path: prefix + "*" + suffix, Err: ErrExist} + } + return f, err + } +} + +var errPatternHasSeparator = errors.New("pattern contains path separator") + +// prefixAndSuffix splits pattern by the last wildcard "*", if applicable, +// returning prefix as the part before "*" and suffix as the part after "*". +func prefixAndSuffix(pattern string) (prefix, suffix string, err error) { + for i := 0; i < len(pattern); i++ { + if IsPathSeparator(pattern[i]) { + return "", "", errPatternHasSeparator + } + } + if pos := lastIndex(pattern, '*'); pos != -1 { + prefix, suffix = pattern[:pos], pattern[pos+1:] + } else { + prefix = pattern + } + return prefix, suffix, nil +} + +// MkdirTemp creates a new temporary directory in the directory dir +// and returns the pathname of the new directory. +// The new directory's name is generated by adding a random string to the end of pattern. +// If pattern includes a "*", the random string replaces the last "*" instead. +// If dir is the empty string, MkdirTemp uses the default directory for temporary files, as returned by TempDir. +// Multiple programs or goroutines calling MkdirTemp simultaneously will not choose the same directory. +// It is the caller's responsibility to remove the directory when it is no longer needed. +func MkdirTemp(dir, pattern string) (string, error) { + if dir == "" { + dir = TempDir() + } + + prefix, suffix, err := prefixAndSuffix(pattern) + if err != nil { + return "", &PathError{Op: "mkdirtemp", Path: pattern, Err: err} + } + prefix = joinPath(dir, prefix) + + try := 0 + for { + name := prefix + nextRandom() + suffix + err := Mkdir(name, 0700) + if err == nil { + return name, nil + } + if IsExist(err) { + if try++; try < 10000 { + continue + } + return "", &PathError{Op: "mkdirtemp", Path: dir + string(PathSeparator) + prefix + "*" + suffix, Err: ErrExist} + } + if IsNotExist(err) { + if _, err := Stat(dir); IsNotExist(err) { + return "", err + } + } + return "", err + } +} + +func joinPath(dir, name string) string { + if len(dir) > 0 && IsPathSeparator(dir[len(dir)-1]) { + return dir + name + } + return dir + string(PathSeparator) + name +} + +// lastIndex from the strings package. +func lastIndex(s string, sep byte) int { + for i := len(s) - 1; i >= 0; i-- { + if s[i] == sep { + return i + } + } + return -1 +} diff --git a/src/os/tempfile_test.go b/src/os/tempfile_test.go new file mode 100644 index 0000000..82f0aab --- /dev/null +++ b/src/os/tempfile_test.go @@ -0,0 +1,205 @@ +// Copyright 2010 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 os_test + +import ( + "errors" + "io/fs" + . "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +func TestCreateTemp(t *testing.T) { + t.Parallel() + + dir, err := MkdirTemp("", "TestCreateTempBadDir") + if err != nil { + t.Fatal(err) + } + defer RemoveAll(dir) + + nonexistentDir := filepath.Join(dir, "_not_exists_") + f, err := CreateTemp(nonexistentDir, "foo") + if f != nil || err == nil { + t.Errorf("CreateTemp(%q, `foo`) = %v, %v", nonexistentDir, f, err) + } +} + +func TestCreateTempPattern(t *testing.T) { + t.Parallel() + + tests := []struct{ pattern, prefix, suffix string }{ + {"tempfile_test", "tempfile_test", ""}, + {"tempfile_test*", "tempfile_test", ""}, + {"tempfile_test*xyz", "tempfile_test", "xyz"}, + } + for _, test := range tests { + f, err := CreateTemp("", test.pattern) + if err != nil { + t.Errorf("CreateTemp(..., %q) error: %v", test.pattern, err) + continue + } + defer Remove(f.Name()) + base := filepath.Base(f.Name()) + f.Close() + if !(strings.HasPrefix(base, test.prefix) && strings.HasSuffix(base, test.suffix)) { + t.Errorf("CreateTemp pattern %q created bad name %q; want prefix %q & suffix %q", + test.pattern, base, test.prefix, test.suffix) + } + } +} + +func TestCreateTempBadPattern(t *testing.T) { + t.Parallel() + + tmpDir, err := MkdirTemp("", t.Name()) + if err != nil { + t.Fatal(err) + } + defer RemoveAll(tmpDir) + + const sep = string(PathSeparator) + tests := []struct { + pattern string + wantErr bool + }{ + {"ioutil*test", false}, + {"tempfile_test*foo", false}, + {"tempfile_test" + sep + "foo", true}, + {"tempfile_test*" + sep + "foo", true}, + {"tempfile_test" + sep + "*foo", true}, + {sep + "tempfile_test" + sep + "*foo", true}, + {"tempfile_test*foo" + sep, true}, + } + for _, tt := range tests { + t.Run(tt.pattern, func(t *testing.T) { + tmpfile, err := CreateTemp(tmpDir, tt.pattern) + if tmpfile != nil { + defer tmpfile.Close() + } + if tt.wantErr { + if err == nil { + t.Errorf("CreateTemp(..., %#q) succeeded, expected error", tt.pattern) + } + if !errors.Is(err, ErrPatternHasSeparator) { + t.Errorf("CreateTemp(..., %#q): %v, expected ErrPatternHasSeparator", tt.pattern, err) + } + } else if err != nil { + t.Errorf("CreateTemp(..., %#q): %v", tt.pattern, err) + } + }) + } +} + +func TestMkdirTemp(t *testing.T) { + t.Parallel() + + name, err := MkdirTemp("/_not_exists_", "foo") + if name != "" || err == nil { + t.Errorf("MkdirTemp(`/_not_exists_`, `foo`) = %v, %v", name, err) + } + + tests := []struct { + pattern string + wantPrefix, wantSuffix string + }{ + {"tempfile_test", "tempfile_test", ""}, + {"tempfile_test*", "tempfile_test", ""}, + {"tempfile_test*xyz", "tempfile_test", "xyz"}, + } + + dir := filepath.Clean(TempDir()) + + runTestMkdirTemp := func(t *testing.T, pattern, wantRePat string) { + name, err := MkdirTemp(dir, pattern) + if name == "" || err != nil { + t.Fatalf("MkdirTemp(dir, `tempfile_test`) = %v, %v", name, err) + } + defer Remove(name) + + re := regexp.MustCompile(wantRePat) + if !re.MatchString(name) { + t.Errorf("MkdirTemp(%q, %q) created bad name\n\t%q\ndid not match pattern\n\t%q", dir, pattern, name, wantRePat) + } + } + + for _, tt := range tests { + t.Run(tt.pattern, func(t *testing.T) { + wantRePat := "^" + regexp.QuoteMeta(filepath.Join(dir, tt.wantPrefix)) + "[0-9]+" + regexp.QuoteMeta(tt.wantSuffix) + "$" + runTestMkdirTemp(t, tt.pattern, wantRePat) + }) + } + + // Separately testing "*xyz" (which has no prefix). That is when constructing the + // pattern to assert on, as in the previous loop, using filepath.Join for an empty + // prefix filepath.Join(dir, ""), produces the pattern: + // ^[0-9]+xyz$ + // yet we just want to match + // "^/[0-9]+xyz" + t.Run("*xyz", func(t *testing.T) { + wantRePat := "^" + regexp.QuoteMeta(filepath.Join(dir)) + regexp.QuoteMeta(string(filepath.Separator)) + "[0-9]+xyz$" + runTestMkdirTemp(t, "*xyz", wantRePat) + }) +} + +// test that we return a nice error message if the dir argument to TempDir doesn't +// exist (or that it's empty and TempDir doesn't exist) +func TestMkdirTempBadDir(t *testing.T) { + t.Parallel() + + dir, err := MkdirTemp("", "MkdirTempBadDir") + if err != nil { + t.Fatal(err) + } + defer RemoveAll(dir) + + badDir := filepath.Join(dir, "not-exist") + _, err = MkdirTemp(badDir, "foo") + if pe, ok := err.(*fs.PathError); !ok || !IsNotExist(err) || pe.Path != badDir { + t.Errorf("TempDir error = %#v; want PathError for path %q satisfying IsNotExist", err, badDir) + } +} + +func TestMkdirTempBadPattern(t *testing.T) { + t.Parallel() + + tmpDir, err := MkdirTemp("", t.Name()) + if err != nil { + t.Fatal(err) + } + defer RemoveAll(tmpDir) + + const sep = string(PathSeparator) + tests := []struct { + pattern string + wantErr bool + }{ + {"ioutil*test", false}, + {"tempfile_test*foo", false}, + {"tempfile_test" + sep + "foo", true}, + {"tempfile_test*" + sep + "foo", true}, + {"tempfile_test" + sep + "*foo", true}, + {sep + "tempfile_test" + sep + "*foo", true}, + {"tempfile_test*foo" + sep, true}, + } + for _, tt := range tests { + t.Run(tt.pattern, func(t *testing.T) { + _, err := MkdirTemp(tmpDir, tt.pattern) + if tt.wantErr { + if err == nil { + t.Errorf("MkdirTemp(..., %#q) succeeded, expected error", tt.pattern) + } + if !errors.Is(err, ErrPatternHasSeparator) { + t.Errorf("MkdirTemp(..., %#q): %v, expected ErrPatternHasSeparator", tt.pattern, err) + } + } else if err != nil { + t.Errorf("MkdirTemp(..., %#q): %v", tt.pattern, err) + } + }) + } +} diff --git a/src/os/testdata/dirfs/a b/src/os/testdata/dirfs/a new file mode 100644 index 0000000..e69de29 diff --git a/src/os/testdata/dirfs/b b/src/os/testdata/dirfs/b new file mode 100644 index 0000000..e69de29 diff --git a/src/os/testdata/dirfs/dir/x b/src/os/testdata/dirfs/dir/x new file mode 100644 index 0000000..e69de29 diff --git a/src/os/testdata/hello b/src/os/testdata/hello new file mode 100644 index 0000000..e47c092 --- /dev/null +++ b/src/os/testdata/hello @@ -0,0 +1 @@ +Hello, Gophers! diff --git a/src/os/testdata/issue37161/a b/src/os/testdata/issue37161/a new file mode 100644 index 0000000..7898192 --- /dev/null +++ b/src/os/testdata/issue37161/a @@ -0,0 +1 @@ +a diff --git a/src/os/testdata/issue37161/b b/src/os/testdata/issue37161/b new file mode 100644 index 0000000..6178079 --- /dev/null +++ b/src/os/testdata/issue37161/b @@ -0,0 +1 @@ +b diff --git a/src/os/testdata/issue37161/c b/src/os/testdata/issue37161/c new file mode 100644 index 0000000..f2ad6c7 --- /dev/null +++ b/src/os/testdata/issue37161/c @@ -0,0 +1 @@ +c diff --git a/src/os/timeout_test.go b/src/os/timeout_test.go new file mode 100644 index 0000000..e0d2328 --- /dev/null +++ b/src/os/timeout_test.go @@ -0,0 +1,708 @@ +// Copyright 2017 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. + +//go:build !js && !plan9 && !wasip1 && !windows + +package os_test + +import ( + "fmt" + "io" + "math/rand" + "os" + "os/signal" + "runtime" + "sync" + "syscall" + "testing" + "time" +) + +func TestNonpollableDeadline(t *testing.T) { + // On BSD systems regular files seem to be pollable, + // so just run this test on Linux. + if runtime.GOOS != "linux" { + t.Skipf("skipping on %s", runtime.GOOS) + } + t.Parallel() + + f, err := os.CreateTemp("", "ostest") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + defer f.Close() + deadline := time.Now().Add(10 * time.Second) + if err := f.SetDeadline(deadline); err != os.ErrNoDeadline { + t.Errorf("SetDeadline on file returned %v, wanted %v", err, os.ErrNoDeadline) + } + if err := f.SetReadDeadline(deadline); err != os.ErrNoDeadline { + t.Errorf("SetReadDeadline on file returned %v, wanted %v", err, os.ErrNoDeadline) + } + if err := f.SetWriteDeadline(deadline); err != os.ErrNoDeadline { + t.Errorf("SetWriteDeadline on file returned %v, wanted %v", err, os.ErrNoDeadline) + } +} + +// noDeadline is a zero time.Time value, which cancels a deadline. +var noDeadline time.Time + +var readTimeoutTests = []struct { + timeout time.Duration + xerrs [2]error // expected errors in transition +}{ + // Tests that read deadlines work, even if there's data ready + // to be read. + {-5 * time.Second, [2]error{os.ErrDeadlineExceeded, os.ErrDeadlineExceeded}}, + + {50 * time.Millisecond, [2]error{nil, os.ErrDeadlineExceeded}}, +} + +// There is a very similar copy of this in net/timeout_test.go. +func TestReadTimeout(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + if _, err := w.Write([]byte("READ TIMEOUT TEST")); err != nil { + t.Fatal(err) + } + + for i, tt := range readTimeoutTests { + if err := r.SetReadDeadline(time.Now().Add(tt.timeout)); err != nil { + t.Fatalf("#%d: %v", i, err) + } + var b [1]byte + for j, xerr := range tt.xerrs { + for { + n, err := r.Read(b[:]) + if xerr != nil { + if !isDeadlineExceeded(err) { + t.Fatalf("#%d/%d: %v", i, j, err) + } + } + if err == nil { + time.Sleep(tt.timeout / 3) + continue + } + if n != 0 { + t.Fatalf("#%d/%d: read %d; want 0", i, j, n) + } + break + } + } + } +} + +// There is a very similar copy of this in net/timeout_test.go. +func TestReadTimeoutMustNotReturn(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + max := time.NewTimer(100 * time.Millisecond) + defer max.Stop() + ch := make(chan error) + go func() { + if err := r.SetDeadline(time.Now().Add(-5 * time.Second)); err != nil { + t.Error(err) + } + if err := r.SetWriteDeadline(time.Now().Add(-5 * time.Second)); err != nil { + t.Error(err) + } + if err := r.SetReadDeadline(noDeadline); err != nil { + t.Error(err) + } + var b [1]byte + _, err := r.Read(b[:]) + ch <- err + }() + + select { + case err := <-ch: + t.Fatalf("expected Read to not return, but it returned with %v", err) + case <-max.C: + w.Close() + err := <-ch // wait for tester goroutine to stop + if os.IsTimeout(err) { + t.Fatal(err) + } + } +} + +var writeTimeoutTests = []struct { + timeout time.Duration + xerrs [2]error // expected errors in transition +}{ + // Tests that write deadlines work, even if there's buffer + // space available to write. + {-5 * time.Second, [2]error{os.ErrDeadlineExceeded, os.ErrDeadlineExceeded}}, + + {10 * time.Millisecond, [2]error{nil, os.ErrDeadlineExceeded}}, +} + +// There is a very similar copy of this in net/timeout_test.go. +func TestWriteTimeout(t *testing.T) { + t.Parallel() + + for i, tt := range writeTimeoutTests { + t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + if err := w.SetWriteDeadline(time.Now().Add(tt.timeout)); err != nil { + t.Fatalf("%v", err) + } + for j, xerr := range tt.xerrs { + for { + n, err := w.Write([]byte("WRITE TIMEOUT TEST")) + if xerr != nil { + if !isDeadlineExceeded(err) { + t.Fatalf("%d: %v", j, err) + } + } + if err == nil { + time.Sleep(tt.timeout / 3) + continue + } + if n != 0 { + t.Fatalf("%d: wrote %d; want 0", j, n) + } + break + } + } + }) + } +} + +// There is a very similar copy of this in net/timeout_test.go. +func TestWriteTimeoutMustNotReturn(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + max := time.NewTimer(100 * time.Millisecond) + defer max.Stop() + ch := make(chan error) + go func() { + if err := w.SetDeadline(time.Now().Add(-5 * time.Second)); err != nil { + t.Error(err) + } + if err := w.SetReadDeadline(time.Now().Add(-5 * time.Second)); err != nil { + t.Error(err) + } + if err := w.SetWriteDeadline(noDeadline); err != nil { + t.Error(err) + } + var b [1]byte + for { + if _, err := w.Write(b[:]); err != nil { + ch <- err + break + } + } + }() + + select { + case err := <-ch: + t.Fatalf("expected Write to not return, but it returned with %v", err) + case <-max.C: + r.Close() + err := <-ch // wait for tester goroutine to stop + if os.IsTimeout(err) { + t.Fatal(err) + } + } +} + +const ( + // minDynamicTimeout is the minimum timeout to attempt for + // tests that automatically increase timeouts until success. + // + // Lower values may allow tests to succeed more quickly if the value is close + // to the true minimum, but may require more iterations (and waste more time + // and CPU power on failed attempts) if the timeout is too low. + minDynamicTimeout = 1 * time.Millisecond + + // maxDynamicTimeout is the maximum timeout to attempt for + // tests that automatically increase timeouts until success. + // + // This should be a strict upper bound on the latency required to hit a + // timeout accurately, even on a slow or heavily-loaded machine. If a test + // would increase the timeout beyond this value, the test fails. + maxDynamicTimeout = 4 * time.Second +) + +// timeoutUpperBound returns the maximum time that we expect a timeout of +// duration d to take to return the caller. +func timeoutUpperBound(d time.Duration) time.Duration { + switch runtime.GOOS { + case "openbsd", "netbsd": + // NetBSD and OpenBSD seem to be unable to reliably hit deadlines even when + // the absolute durations are long. + // In https://build.golang.org/log/c34f8685d020b98377dd4988cd38f0c5bd72267e, + // we observed that an openbsd-amd64-68 builder took 4.090948779s for a + // 2.983020682s timeout (37.1% overhead). + // (See https://go.dev/issue/50189 for further detail.) + // Give them lots of slop to compensate. + return d * 3 / 2 + } + // Other platforms seem to hit their deadlines more reliably, + // at least when they are long enough to cover scheduling jitter. + return d * 11 / 10 +} + +// nextTimeout returns the next timeout to try after an operation took the given +// actual duration with a timeout shorter than that duration. +func nextTimeout(actual time.Duration) (next time.Duration, ok bool) { + if actual >= maxDynamicTimeout { + return maxDynamicTimeout, false + } + // Since the previous attempt took actual, we can't expect to beat that + // duration by any significant margin. Try the next attempt with an arbitrary + // factor above that, so that our growth curve is at least exponential. + next = actual * 5 / 4 + if next > maxDynamicTimeout { + return maxDynamicTimeout, true + } + return next, true +} + +// There is a very similar copy of this in net/timeout_test.go. +func TestReadTimeoutFluctuation(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + d := minDynamicTimeout + b := make([]byte, 256) + for { + t.Logf("SetReadDeadline(+%v)", d) + t0 := time.Now() + deadline := t0.Add(d) + if err = r.SetReadDeadline(deadline); err != nil { + t.Fatalf("SetReadDeadline(%v): %v", deadline, err) + } + var n int + n, err = r.Read(b) + t1 := time.Now() + + if n != 0 || err == nil || !isDeadlineExceeded(err) { + t.Errorf("Read did not return (0, timeout): (%d, %v)", n, err) + } + + actual := t1.Sub(t0) + if t1.Before(deadline) { + t.Errorf("Read took %s; expected at least %s", actual, d) + } + if t.Failed() { + return + } + if want := timeoutUpperBound(d); actual > want { + next, ok := nextTimeout(actual) + if !ok { + t.Fatalf("Read took %s; expected at most %v", actual, want) + } + // Maybe this machine is too slow to reliably schedule goroutines within + // the requested duration. Increase the timeout and try again. + t.Logf("Read took %s (expected %s); trying with longer timeout", actual, d) + d = next + continue + } + + break + } +} + +// There is a very similar copy of this in net/timeout_test.go. +func TestWriteTimeoutFluctuation(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + d := minDynamicTimeout + for { + t.Logf("SetWriteDeadline(+%v)", d) + t0 := time.Now() + deadline := t0.Add(d) + if err := w.SetWriteDeadline(deadline); err != nil { + t.Fatalf("SetWriteDeadline(%v): %v", deadline, err) + } + var n int64 + var err error + for { + var dn int + dn, err = w.Write([]byte("TIMEOUT TRANSMITTER")) + n += int64(dn) + if err != nil { + break + } + } + t1 := time.Now() + // Inv: err != nil + if !isDeadlineExceeded(err) { + t.Fatalf("Write did not return (any, timeout): (%d, %v)", n, err) + } + + actual := t1.Sub(t0) + if t1.Before(deadline) { + t.Errorf("Write took %s; expected at least %s", actual, d) + } + if t.Failed() { + return + } + if want := timeoutUpperBound(d); actual > want { + if n > 0 { + // SetWriteDeadline specifies a time “after which I/O operations fail + // instead of blocking”. However, the kernel's send buffer is not yet + // full, we may be able to write some arbitrary (but finite) number of + // bytes to it without blocking. + t.Logf("Wrote %d bytes into send buffer; retrying until buffer is full", n) + if d <= maxDynamicTimeout/2 { + // We don't know how long the actual write loop would have taken if + // the buffer were full, so just guess and double the duration so that + // the next attempt can make twice as much progress toward filling it. + d *= 2 + } + } else if next, ok := nextTimeout(actual); !ok { + t.Fatalf("Write took %s; expected at most %s", actual, want) + } else { + // Maybe this machine is too slow to reliably schedule goroutines within + // the requested duration. Increase the timeout and try again. + t.Logf("Write took %s (expected %s); trying with longer timeout", actual, d) + d = next + } + continue + } + + break + } +} + +// There is a very similar copy of this in net/timeout_test.go. +func TestVariousDeadlines(t *testing.T) { + t.Parallel() + testVariousDeadlines(t) +} + +// There is a very similar copy of this in net/timeout_test.go. +func TestVariousDeadlines1Proc(t *testing.T) { + // Cannot use t.Parallel - modifies global GOMAXPROCS. + if testing.Short() { + t.Skip("skipping in short mode") + } + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) + testVariousDeadlines(t) +} + +// There is a very similar copy of this in net/timeout_test.go. +func TestVariousDeadlines4Proc(t *testing.T) { + // Cannot use t.Parallel - modifies global GOMAXPROCS. + if testing.Short() { + t.Skip("skipping in short mode") + } + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4)) + testVariousDeadlines(t) +} + +type neverEnding byte + +func (b neverEnding) Read(p []byte) (int, error) { + for i := range p { + p[i] = byte(b) + } + return len(p), nil +} + +func testVariousDeadlines(t *testing.T) { + type result struct { + n int64 + err error + d time.Duration + } + + handler := func(w *os.File, pasvch chan result) { + // The writer, with no timeouts of its own, + // sending bytes to clients as fast as it can. + t0 := time.Now() + n, err := io.Copy(w, neverEnding('a')) + dt := time.Since(t0) + pasvch <- result{n, err, dt} + } + + for _, timeout := range []time.Duration{ + 1 * time.Nanosecond, + 2 * time.Nanosecond, + 5 * time.Nanosecond, + 50 * time.Nanosecond, + 100 * time.Nanosecond, + 200 * time.Nanosecond, + 500 * time.Nanosecond, + 750 * time.Nanosecond, + 1 * time.Microsecond, + 5 * time.Microsecond, + 25 * time.Microsecond, + 250 * time.Microsecond, + 500 * time.Microsecond, + 1 * time.Millisecond, + 5 * time.Millisecond, + 100 * time.Millisecond, + 250 * time.Millisecond, + 500 * time.Millisecond, + 1 * time.Second, + } { + numRuns := 3 + if testing.Short() { + numRuns = 1 + if timeout > 500*time.Microsecond { + continue + } + } + for run := 0; run < numRuns; run++ { + t.Run(fmt.Sprintf("%v-%d", timeout, run+1), func(t *testing.T) { + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + pasvch := make(chan result) + go handler(w, pasvch) + + tooLong := 5 * time.Second + max := time.NewTimer(tooLong) + defer max.Stop() + actvch := make(chan result) + go func() { + t0 := time.Now() + if err := r.SetDeadline(t0.Add(timeout)); err != nil { + t.Error(err) + } + n, err := io.Copy(io.Discard, r) + dt := time.Since(t0) + r.Close() + actvch <- result{n, err, dt} + }() + + select { + case res := <-actvch: + if !isDeadlineExceeded(err) { + t.Logf("good client timeout after %v, reading %d bytes", res.d, res.n) + } else { + t.Fatalf("client Copy = %d, %v; want timeout", res.n, res.err) + } + case <-max.C: + t.Fatalf("timeout (%v) waiting for client to timeout (%v) reading", tooLong, timeout) + } + + select { + case res := <-pasvch: + t.Logf("writer in %v wrote %d: %v", res.d, res.n, res.err) + case <-max.C: + t.Fatalf("timeout waiting for writer to finish writing") + } + }) + } + } +} + +// There is a very similar copy of this in net/timeout_test.go. +func TestReadWriteDeadlineRace(t *testing.T) { + t.Parallel() + + N := 1000 + if testing.Short() { + N = 50 + } + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + var wg sync.WaitGroup + wg.Add(3) + go func() { + defer wg.Done() + tic := time.NewTicker(2 * time.Microsecond) + defer tic.Stop() + for i := 0; i < N; i++ { + if err := r.SetReadDeadline(time.Now().Add(2 * time.Microsecond)); err != nil { + break + } + if err := w.SetWriteDeadline(time.Now().Add(2 * time.Microsecond)); err != nil { + break + } + <-tic.C + } + }() + go func() { + defer wg.Done() + var b [1]byte + for i := 0; i < N; i++ { + _, err := r.Read(b[:]) + if err != nil && !isDeadlineExceeded(err) { + t.Error("Read returned non-timeout error", err) + } + } + }() + go func() { + defer wg.Done() + var b [1]byte + for i := 0; i < N; i++ { + _, err := w.Write(b[:]) + if err != nil && !isDeadlineExceeded(err) { + t.Error("Write returned non-timeout error", err) + } + } + }() + wg.Wait() // wait for tester goroutine to stop +} + +// TestRacyRead tests that it is safe to mutate the input Read buffer +// immediately after cancellation has occurred. +func TestRacyRead(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + var wg sync.WaitGroup + defer wg.Wait() + + go io.Copy(w, rand.New(rand.NewSource(0))) + + r.SetReadDeadline(time.Now().Add(time.Millisecond)) + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + b1 := make([]byte, 1024) + b2 := make([]byte, 1024) + for j := 0; j < 100; j++ { + _, err := r.Read(b1) + copy(b1, b2) // Mutate b1 to trigger potential race + if err != nil { + if !isDeadlineExceeded(err) { + t.Error(err) + } + r.SetReadDeadline(time.Now().Add(time.Millisecond)) + } + } + }() + } +} + +// TestRacyWrite tests that it is safe to mutate the input Write buffer +// immediately after cancellation has occurred. +func TestRacyWrite(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + var wg sync.WaitGroup + defer wg.Wait() + + go io.Copy(io.Discard, r) + + w.SetWriteDeadline(time.Now().Add(time.Millisecond)) + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + b1 := make([]byte, 1024) + b2 := make([]byte, 1024) + for j := 0; j < 100; j++ { + _, err := w.Write(b1) + copy(b1, b2) // Mutate b1 to trigger potential race + if err != nil { + if !isDeadlineExceeded(err) { + t.Error(err) + } + w.SetWriteDeadline(time.Now().Add(time.Millisecond)) + } + } + }() + } +} + +// Closing a TTY while reading from it should not hang. Issue 23943. +func TestTTYClose(t *testing.T) { + // Ignore SIGTTIN in case we are running in the background. + signal.Ignore(syscall.SIGTTIN) + defer signal.Reset(syscall.SIGTTIN) + + f, err := os.Open("/dev/tty") + if err != nil { + t.Skipf("skipping because opening /dev/tty failed: %v", err) + } + + go func() { + var buf [1]byte + f.Read(buf[:]) + }() + + // Give the goroutine a chance to enter the read. + // It doesn't matter much if it occasionally fails to do so, + // we won't be testing what we want to test but the test will pass. + time.Sleep(time.Millisecond) + + c := make(chan bool) + go func() { + defer close(c) + f.Close() + }() + + select { + case <-c: + case <-time.After(time.Second): + t.Error("timed out waiting for close") + } + + // On some systems the goroutines may now be hanging. + // There's not much we can do about that. +} diff --git a/src/os/types.go b/src/os/types.go new file mode 100644 index 0000000..d8edd98 --- /dev/null +++ b/src/os/types.go @@ -0,0 +1,74 @@ +// Copyright 2009 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 os + +import ( + "io/fs" + "syscall" +) + +// Getpagesize returns the underlying system's memory page size. +func Getpagesize() int { return syscall.Getpagesize() } + +// File represents an open file descriptor. +type File struct { + *file // os specific +} + +// A FileInfo describes a file and is returned by Stat and Lstat. +type FileInfo = fs.FileInfo + +// 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 = fs.FileMode + +// 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 = fs.ModeDir // d: is a directory + ModeAppend = fs.ModeAppend // a: append-only + ModeExclusive = fs.ModeExclusive // l: exclusive use + ModeTemporary = fs.ModeTemporary // T: temporary file; Plan 9 only + ModeSymlink = fs.ModeSymlink // L: symbolic link + ModeDevice = fs.ModeDevice // D: device file + ModeNamedPipe = fs.ModeNamedPipe // p: named pipe (FIFO) + ModeSocket = fs.ModeSocket // S: Unix domain socket + ModeSetuid = fs.ModeSetuid // u: setuid + ModeSetgid = fs.ModeSetgid // g: setgid + ModeCharDevice = fs.ModeCharDevice // c: Unix character device, when ModeDevice is set + ModeSticky = fs.ModeSticky // t: sticky + ModeIrregular = fs.ModeIrregular // ?: non-regular file; nothing else is known about this file + + // Mask for the type bits. For regular files, none will be set. + ModeType = fs.ModeType + + ModePerm = fs.ModePerm // Unix permission bits, 0o777 +) + +func (fs *fileStat) Name() string { return fs.name } +func (fs *fileStat) IsDir() bool { return fs.Mode().IsDir() } + +// SameFile reports whether fi1 and fi2 describe the same file. +// For example, on Unix this means that the device and inode fields +// of the two underlying structures are identical; on other systems +// the decision may be based on the path names. +// SameFile only applies to results returned by this package's Stat. +// It returns false in other cases. +func SameFile(fi1, fi2 FileInfo) bool { + fs1, ok1 := fi1.(*fileStat) + fs2, ok2 := fi2.(*fileStat) + if !ok1 || !ok2 { + return false + } + return sameFile(fs1, fs2) +} diff --git a/src/os/types_plan9.go b/src/os/types_plan9.go new file mode 100644 index 0000000..adb4013 --- /dev/null +++ b/src/os/types_plan9.go @@ -0,0 +1,30 @@ +// Copyright 2009 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 os + +import ( + "syscall" + "time" +) + +// A fileStat is the implementation of FileInfo returned by Stat and Lstat. +type fileStat struct { + name string + size int64 + mode FileMode + modTime time.Time + sys any +} + +func (fs *fileStat) Size() int64 { return fs.size } +func (fs *fileStat) Mode() FileMode { return fs.mode } +func (fs *fileStat) ModTime() time.Time { return fs.modTime } +func (fs *fileStat) Sys() any { return fs.sys } + +func sameFile(fs1, fs2 *fileStat) bool { + a := fs1.sys.(*syscall.Dir) + b := fs2.sys.(*syscall.Dir) + return a.Qid.Path == b.Qid.Path && a.Type == b.Type && a.Dev == b.Dev +} diff --git a/src/os/types_unix.go b/src/os/types_unix.go new file mode 100644 index 0000000..1b90a5a --- /dev/null +++ b/src/os/types_unix.go @@ -0,0 +1,30 @@ +// Copyright 2009 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. + +//go:build !windows && !plan9 + +package os + +import ( + "syscall" + "time" +) + +// A fileStat is the implementation of FileInfo returned by Stat and Lstat. +type fileStat struct { + name string + size int64 + mode FileMode + modTime time.Time + sys syscall.Stat_t +} + +func (fs *fileStat) Size() int64 { return fs.size } +func (fs *fileStat) Mode() FileMode { return fs.mode } +func (fs *fileStat) ModTime() time.Time { return fs.modTime } +func (fs *fileStat) Sys() any { return &fs.sys } + +func sameFile(fs1, fs2 *fileStat) bool { + return fs1.sys.Dev == fs2.sys.Dev && fs1.sys.Ino == fs2.sys.Ino +} diff --git a/src/os/types_windows.go b/src/os/types_windows.go new file mode 100644 index 0000000..effb014 --- /dev/null +++ b/src/os/types_windows.go @@ -0,0 +1,262 @@ +// Copyright 2009 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 os + +import ( + "internal/syscall/windows" + "sync" + "syscall" + "time" + "unsafe" +) + +// A fileStat is the implementation of FileInfo returned by Stat and Lstat. +type fileStat struct { + name string + + // from ByHandleFileInformation, Win32FileAttributeData and Win32finddata + FileAttributes uint32 + CreationTime syscall.Filetime + LastAccessTime syscall.Filetime + LastWriteTime syscall.Filetime + FileSizeHigh uint32 + FileSizeLow uint32 + + // from Win32finddata + ReparseTag uint32 + + // what syscall.GetFileType returns + filetype uint32 + + // used to implement SameFile + sync.Mutex + path string + vol uint32 + idxhi uint32 + idxlo uint32 + appendNameToPath bool +} + +// newFileStatFromGetFileInformationByHandle calls GetFileInformationByHandle +// to gather all required information about the file handle h. +func newFileStatFromGetFileInformationByHandle(path string, h syscall.Handle) (fs *fileStat, err error) { + var d syscall.ByHandleFileInformation + err = syscall.GetFileInformationByHandle(h, &d) + if err != nil { + return nil, &PathError{Op: "GetFileInformationByHandle", Path: path, Err: err} + } + + var ti windows.FILE_ATTRIBUTE_TAG_INFO + err = windows.GetFileInformationByHandleEx(h, windows.FileAttributeTagInfo, (*byte)(unsafe.Pointer(&ti)), uint32(unsafe.Sizeof(ti))) + if err != nil { + if errno, ok := err.(syscall.Errno); ok && errno == windows.ERROR_INVALID_PARAMETER { + // It appears calling GetFileInformationByHandleEx with + // FILE_ATTRIBUTE_TAG_INFO fails on FAT file system with + // ERROR_INVALID_PARAMETER. Clear ti.ReparseTag in that + // instance to indicate no symlinks are possible. + ti.ReparseTag = 0 + } else { + return nil, &PathError{Op: "GetFileInformationByHandleEx", Path: path, Err: err} + } + } + + return &fileStat{ + name: basename(path), + FileAttributes: d.FileAttributes, + CreationTime: d.CreationTime, + LastAccessTime: d.LastAccessTime, + LastWriteTime: d.LastWriteTime, + FileSizeHigh: d.FileSizeHigh, + FileSizeLow: d.FileSizeLow, + vol: d.VolumeSerialNumber, + idxhi: d.FileIndexHigh, + idxlo: d.FileIndexLow, + ReparseTag: ti.ReparseTag, + // fileStat.path is used by os.SameFile to decide if it needs + // to fetch vol, idxhi and idxlo. But these are already set, + // so set fileStat.path to "" to prevent os.SameFile doing it again. + }, nil +} + +// newFileStatFromWin32finddata copies all required information +// from syscall.Win32finddata d into the newly created fileStat. +func newFileStatFromWin32finddata(d *syscall.Win32finddata) *fileStat { + fs := &fileStat{ + FileAttributes: d.FileAttributes, + CreationTime: d.CreationTime, + LastAccessTime: d.LastAccessTime, + LastWriteTime: d.LastWriteTime, + FileSizeHigh: d.FileSizeHigh, + FileSizeLow: d.FileSizeLow, + } + if d.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 { + // Per https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-win32_find_dataw: + // “If the dwFileAttributes member includes the FILE_ATTRIBUTE_REPARSE_POINT + // attribute, this member specifies the reparse point tag. Otherwise, this + // value is undefined and should not be used.” + fs.ReparseTag = d.Reserved0 + } + return fs +} + +func (fs *fileStat) isSymlink() bool { + // As of https://go.dev/cl/86556, we treat MOUNT_POINT reparse points as + // symlinks because otherwise certain directory junction tests in the + // path/filepath package would fail. + // + // However, + // https://learn.microsoft.com/en-us/windows/win32/fileio/hard-links-and-junctions + // seems to suggest that directory junctions should be treated like hard + // links, not symlinks. + // + // TODO(bcmills): Get more input from Microsoft on what the behavior ought to + // be for MOUNT_POINT reparse points. + + return fs.ReparseTag == syscall.IO_REPARSE_TAG_SYMLINK || + fs.ReparseTag == windows.IO_REPARSE_TAG_MOUNT_POINT +} + +func (fs *fileStat) Size() int64 { + return int64(fs.FileSizeHigh)<<32 + int64(fs.FileSizeLow) +} + +func (fs *fileStat) Mode() (m FileMode) { + if fs.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 { + m |= 0444 + } else { + m |= 0666 + } + if fs.isSymlink() { + return m | ModeSymlink + } + if fs.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { + m |= ModeDir | 0111 + } + switch fs.filetype { + case syscall.FILE_TYPE_PIPE: + m |= ModeNamedPipe + case syscall.FILE_TYPE_CHAR: + m |= ModeDevice | ModeCharDevice + } + if fs.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 && m&ModeType == 0 { + if fs.ReparseTag == windows.IO_REPARSE_TAG_DEDUP { + // If the Data Deduplication service is enabled on Windows Server, its + // Optimization job may convert regular files to IO_REPARSE_TAG_DEDUP + // whenever that job runs. + // + // However, DEDUP reparse points remain similar in most respects to + // regular files: they continue to support random-access reads and writes + // of persistent data, and they shouldn't add unexpected latency or + // unavailability in the way that a network filesystem might. + // + // Go programs may use ModeIrregular to filter out unusual files (such as + // raw device files on Linux, POSIX FIFO special files, and so on), so + // to avoid files changing unpredictably from regular to irregular we will + // consider DEDUP files to be close enough to regular to treat as such. + } else { + m |= ModeIrregular + } + } + return m +} + +func (fs *fileStat) ModTime() time.Time { + return time.Unix(0, fs.LastWriteTime.Nanoseconds()) +} + +// Sys returns syscall.Win32FileAttributeData for file fs. +func (fs *fileStat) Sys() any { + return &syscall.Win32FileAttributeData{ + FileAttributes: fs.FileAttributes, + CreationTime: fs.CreationTime, + LastAccessTime: fs.LastAccessTime, + LastWriteTime: fs.LastWriteTime, + FileSizeHigh: fs.FileSizeHigh, + FileSizeLow: fs.FileSizeLow, + } +} + +func (fs *fileStat) loadFileId() error { + fs.Lock() + defer fs.Unlock() + if fs.path == "" { + // already done + return nil + } + var path string + if fs.appendNameToPath { + path = fs.path + `\` + fs.name + } else { + path = fs.path + } + pathp, err := syscall.UTF16PtrFromString(path) + if err != nil { + return err + } + + // Per https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-points-and-file-operations, + // “Applications that use the CreateFile function should specify the + // FILE_FLAG_OPEN_REPARSE_POINT flag when opening the file if it is a reparse + // point.” + // + // And per https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew, + // “If the file is not a reparse point, then this flag is ignored.” + // + // So we set FILE_FLAG_OPEN_REPARSE_POINT unconditionally, since we want + // information about the reparse point itself. + // + // If the file is a symlink, the symlink target should have already been + // resolved when the fileStat was created, so we don't need to worry about + // resolving symlink reparse points again here. + attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS | syscall.FILE_FLAG_OPEN_REPARSE_POINT) + + h, err := syscall.CreateFile(pathp, 0, 0, nil, syscall.OPEN_EXISTING, attrs, 0) + if err != nil { + return err + } + defer syscall.CloseHandle(h) + var i syscall.ByHandleFileInformation + err = syscall.GetFileInformationByHandle(h, &i) + if err != nil { + return err + } + fs.path = "" + fs.vol = i.VolumeSerialNumber + fs.idxhi = i.FileIndexHigh + fs.idxlo = i.FileIndexLow + return nil +} + +// saveInfoFromPath saves full path of the file to be used by os.SameFile later, +// and set name from path. +func (fs *fileStat) saveInfoFromPath(path string) error { + fs.path = path + if !isAbs(fs.path) { + var err error + fs.path, err = syscall.FullPath(fs.path) + if err != nil { + return &PathError{Op: "FullPath", Path: path, Err: err} + } + } + fs.name = basename(path) + return nil +} + +func sameFile(fs1, fs2 *fileStat) bool { + e := fs1.loadFileId() + if e != nil { + return false + } + e = fs2.loadFileId() + if e != nil { + return false + } + return fs1.vol == fs2.vol && fs1.idxhi == fs2.idxhi && fs1.idxlo == fs2.idxlo +} + +// For testing. +func atime(fi FileInfo) time.Time { + return time.Unix(0, fi.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) +} diff --git a/src/os/user/cgo_listgroups_unix.go b/src/os/user/cgo_listgroups_unix.go new file mode 100644 index 0000000..5963695 --- /dev/null +++ b/src/os/user/cgo_listgroups_unix.go @@ -0,0 +1,57 @@ +// Copyright 2016 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. + +//go:build (cgo || darwin) && !osusergo && (darwin || dragonfly || freebsd || (linux && !android) || netbsd || openbsd || (solaris && !illumos)) + +package user + +import ( + "fmt" + "strconv" + "unsafe" +) + +const maxGroups = 2048 + +func listGroups(u *User) ([]string, error) { + ug, err := strconv.Atoi(u.Gid) + if err != nil { + return nil, fmt.Errorf("user: list groups for %s: invalid gid %q", u.Username, u.Gid) + } + userGID := _C_gid_t(ug) + nameC := make([]byte, len(u.Username)+1) + copy(nameC, u.Username) + + n := _C_int(256) + gidsC := make([]_C_gid_t, n) + rv := getGroupList((*_C_char)(unsafe.Pointer(&nameC[0])), userGID, &gidsC[0], &n) + if rv == -1 { + // Mac is the only Unix that does not set n properly when rv == -1, so + // we need to use different logic for Mac vs. the other OS's. + if err := groupRetry(u.Username, nameC, userGID, &gidsC, &n); err != nil { + return nil, err + } + } + gidsC = gidsC[:n] + gids := make([]string, 0, n) + for _, g := range gidsC[:n] { + gids = append(gids, strconv.Itoa(int(g))) + } + return gids, nil +} + +// groupRetry retries getGroupList with much larger size for n. The result is +// stored in gids. +func groupRetry(username string, name []byte, userGID _C_gid_t, gids *[]_C_gid_t, n *_C_int) error { + // More than initial buffer, but now n contains the correct size. + if *n > maxGroups { + return fmt.Errorf("user: %q is a member of more than %d groups", username, maxGroups) + } + *gids = make([]_C_gid_t, *n) + rv := getGroupList((*_C_char)(unsafe.Pointer(&name[0])), userGID, &(*gids)[0], n) + if rv == -1 { + return fmt.Errorf("user: list groups for %s failed", username) + } + return nil +} diff --git a/src/os/user/cgo_lookup_cgo.go b/src/os/user/cgo_lookup_cgo.go new file mode 100644 index 0000000..4f78dca --- /dev/null +++ b/src/os/user/cgo_lookup_cgo.go @@ -0,0 +1,112 @@ +// Copyright 2011 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. + +//go:build cgo && !osusergo && unix && !android && !darwin + +package user + +import ( + "syscall" +) + +/* +#cgo solaris CFLAGS: -D_POSIX_PTHREAD_SEMANTICS +#cgo CFLAGS: -fno-stack-protector +#include +#include +#include +#include +#include +#include + +static struct passwd mygetpwuid_r(int uid, char *buf, size_t buflen, int *found, int *perr) { + struct passwd pwd; + struct passwd *result; + memset (&pwd, 0, sizeof(pwd)); + *perr = getpwuid_r(uid, &pwd, buf, buflen, &result); + *found = result != NULL; + return pwd; +} + +static struct passwd mygetpwnam_r(const char *name, char *buf, size_t buflen, int *found, int *perr) { + struct passwd pwd; + struct passwd *result; + memset(&pwd, 0, sizeof(pwd)); + *perr = getpwnam_r(name, &pwd, buf, buflen, &result); + *found = result != NULL; + return pwd; +} + +static struct group mygetgrgid_r(int gid, char *buf, size_t buflen, int *found, int *perr) { + struct group grp; + struct group *result; + memset(&grp, 0, sizeof(grp)); + *perr = getgrgid_r(gid, &grp, buf, buflen, &result); + *found = result != NULL; + return grp; +} + +static struct group mygetgrnam_r(const char *name, char *buf, size_t buflen, int *found, int *perr) { + struct group grp; + struct group *result; + memset(&grp, 0, sizeof(grp)); + *perr = getgrnam_r(name, &grp, buf, buflen, &result); + *found = result != NULL; + return grp; +} +*/ +import "C" + +type _C_char = C.char +type _C_int = C.int +type _C_gid_t = C.gid_t +type _C_uid_t = C.uid_t +type _C_size_t = C.size_t +type _C_struct_group = C.struct_group +type _C_struct_passwd = C.struct_passwd +type _C_long = C.long + +func _C_pw_uid(p *_C_struct_passwd) _C_uid_t { return p.pw_uid } +func _C_pw_uidp(p *_C_struct_passwd) *_C_uid_t { return &p.pw_uid } +func _C_pw_gid(p *_C_struct_passwd) _C_gid_t { return p.pw_gid } +func _C_pw_gidp(p *_C_struct_passwd) *_C_gid_t { return &p.pw_gid } +func _C_pw_name(p *_C_struct_passwd) *_C_char { return p.pw_name } +func _C_pw_gecos(p *_C_struct_passwd) *_C_char { return p.pw_gecos } +func _C_pw_dir(p *_C_struct_passwd) *_C_char { return p.pw_dir } + +func _C_gr_gid(g *_C_struct_group) _C_gid_t { return g.gr_gid } +func _C_gr_name(g *_C_struct_group) *_C_char { return g.gr_name } + +func _C_GoString(p *_C_char) string { return C.GoString(p) } + +func _C_getpwnam_r(name *_C_char, buf *_C_char, size _C_size_t) (pwd _C_struct_passwd, found bool, errno syscall.Errno) { + var f, e _C_int + pwd = C.mygetpwnam_r(name, buf, size, &f, &e) + return pwd, f != 0, syscall.Errno(e) +} + +func _C_getpwuid_r(uid _C_uid_t, buf *_C_char, size _C_size_t) (pwd _C_struct_passwd, found bool, errno syscall.Errno) { + var f, e _C_int + pwd = C.mygetpwuid_r(_C_int(uid), buf, size, &f, &e) + return pwd, f != 0, syscall.Errno(e) +} + +func _C_getgrnam_r(name *_C_char, buf *_C_char, size _C_size_t) (grp _C_struct_group, found bool, errno syscall.Errno) { + var f, e _C_int + grp = C.mygetgrnam_r(name, buf, size, &f, &e) + return grp, f != 0, syscall.Errno(e) +} + +func _C_getgrgid_r(gid _C_gid_t, buf *_C_char, size _C_size_t) (grp _C_struct_group, found bool, errno syscall.Errno) { + var f, e _C_int + grp = C.mygetgrgid_r(_C_int(gid), buf, size, &f, &e) + return grp, f != 0, syscall.Errno(e) +} + +const ( + _C__SC_GETPW_R_SIZE_MAX = C._SC_GETPW_R_SIZE_MAX + _C__SC_GETGR_R_SIZE_MAX = C._SC_GETGR_R_SIZE_MAX +) + +func _C_sysconf(key _C_int) _C_long { return C.sysconf(key) } diff --git a/src/os/user/cgo_lookup_syscall.go b/src/os/user/cgo_lookup_syscall.go new file mode 100644 index 0000000..321df65 --- /dev/null +++ b/src/os/user/cgo_lookup_syscall.go @@ -0,0 +1,65 @@ +// Copyright 2011 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. + +//go:build !osusergo && darwin + +package user + +import ( + "internal/syscall/unix" + "syscall" +) + +type _C_char = byte +type _C_int = int32 +type _C_gid_t = uint32 +type _C_uid_t = uint32 +type _C_size_t = uintptr +type _C_struct_group = unix.Group +type _C_struct_passwd = unix.Passwd +type _C_long = int64 + +func _C_pw_uid(p *_C_struct_passwd) _C_uid_t { return p.Uid } +func _C_pw_uidp(p *_C_struct_passwd) *_C_uid_t { return &p.Uid } +func _C_pw_gid(p *_C_struct_passwd) _C_gid_t { return p.Gid } +func _C_pw_gidp(p *_C_struct_passwd) *_C_gid_t { return &p.Gid } +func _C_pw_name(p *_C_struct_passwd) *_C_char { return p.Name } +func _C_pw_gecos(p *_C_struct_passwd) *_C_char { return p.Gecos } +func _C_pw_dir(p *_C_struct_passwd) *_C_char { return p.Dir } + +func _C_gr_gid(g *_C_struct_group) _C_gid_t { return g.Gid } +func _C_gr_name(g *_C_struct_group) *_C_char { return g.Name } + +func _C_GoString(p *_C_char) string { return unix.GoString(p) } + +func _C_getpwnam_r(name *_C_char, buf *_C_char, size _C_size_t) (pwd _C_struct_passwd, found bool, errno syscall.Errno) { + var result *_C_struct_passwd + errno = unix.Getpwnam(name, &pwd, buf, size, &result) + return pwd, result != nil, errno +} + +func _C_getpwuid_r(uid _C_uid_t, buf *_C_char, size _C_size_t) (pwd _C_struct_passwd, found bool, errno syscall.Errno) { + var result *_C_struct_passwd + errno = unix.Getpwuid(uid, &pwd, buf, size, &result) + return pwd, result != nil, errno +} + +func _C_getgrnam_r(name *_C_char, buf *_C_char, size _C_size_t) (grp _C_struct_group, found bool, errno syscall.Errno) { + var result *_C_struct_group + errno = unix.Getgrnam(name, &grp, buf, size, &result) + return grp, result != nil, errno +} + +func _C_getgrgid_r(gid _C_gid_t, buf *_C_char, size _C_size_t) (grp _C_struct_group, found bool, errno syscall.Errno) { + var result *_C_struct_group + errno = unix.Getgrgid(gid, &grp, buf, size, &result) + return grp, result != nil, errno +} + +const ( + _C__SC_GETPW_R_SIZE_MAX = unix.SC_GETPW_R_SIZE_MAX + _C__SC_GETGR_R_SIZE_MAX = unix.SC_GETGR_R_SIZE_MAX +) + +func _C_sysconf(key _C_int) _C_long { return unix.Sysconf(key) } diff --git a/src/os/user/cgo_lookup_unix.go b/src/os/user/cgo_lookup_unix.go new file mode 100644 index 0000000..3735971 --- /dev/null +++ b/src/os/user/cgo_lookup_unix.go @@ -0,0 +1,200 @@ +// Copyright 2011 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. + +//go:build (cgo || darwin) && !osusergo && unix && !android + +package user + +import ( + "fmt" + "runtime" + "strconv" + "strings" + "syscall" + "unsafe" +) + +func current() (*User, error) { + return lookupUnixUid(syscall.Getuid()) +} + +func lookupUser(username string) (*User, error) { + var pwd _C_struct_passwd + var found bool + nameC := make([]byte, len(username)+1) + copy(nameC, username) + + err := retryWithBuffer(userBuffer, func(buf []byte) syscall.Errno { + var errno syscall.Errno + pwd, found, errno = _C_getpwnam_r((*_C_char)(unsafe.Pointer(&nameC[0])), + (*_C_char)(unsafe.Pointer(&buf[0])), _C_size_t(len(buf))) + return errno + }) + if err != nil { + return nil, fmt.Errorf("user: lookup username %s: %v", username, err) + } + if !found { + return nil, UnknownUserError(username) + } + return buildUser(&pwd), err +} + +func lookupUserId(uid string) (*User, error) { + i, e := strconv.Atoi(uid) + if e != nil { + return nil, e + } + return lookupUnixUid(i) +} + +func lookupUnixUid(uid int) (*User, error) { + var pwd _C_struct_passwd + var found bool + + err := retryWithBuffer(userBuffer, func(buf []byte) syscall.Errno { + var errno syscall.Errno + pwd, found, errno = _C_getpwuid_r(_C_uid_t(uid), + (*_C_char)(unsafe.Pointer(&buf[0])), _C_size_t(len(buf))) + return errno + }) + if err != nil { + return nil, fmt.Errorf("user: lookup userid %d: %v", uid, err) + } + if !found { + return nil, UnknownUserIdError(uid) + } + return buildUser(&pwd), nil +} + +func buildUser(pwd *_C_struct_passwd) *User { + u := &User{ + Uid: strconv.FormatUint(uint64(_C_pw_uid(pwd)), 10), + Gid: strconv.FormatUint(uint64(_C_pw_gid(pwd)), 10), + Username: _C_GoString(_C_pw_name(pwd)), + Name: _C_GoString(_C_pw_gecos(pwd)), + HomeDir: _C_GoString(_C_pw_dir(pwd)), + } + // The pw_gecos field isn't quite standardized. Some docs + // say: "It is expected to be a comma separated list of + // personal data where the first item is the full name of the + // user." + u.Name, _, _ = strings.Cut(u.Name, ",") + return u +} + +func lookupGroup(groupname string) (*Group, error) { + var grp _C_struct_group + var found bool + + cname := make([]byte, len(groupname)+1) + copy(cname, groupname) + + err := retryWithBuffer(groupBuffer, func(buf []byte) syscall.Errno { + var errno syscall.Errno + grp, found, errno = _C_getgrnam_r((*_C_char)(unsafe.Pointer(&cname[0])), + (*_C_char)(unsafe.Pointer(&buf[0])), _C_size_t(len(buf))) + return errno + }) + if err != nil { + return nil, fmt.Errorf("user: lookup groupname %s: %v", groupname, err) + } + if !found { + return nil, UnknownGroupError(groupname) + } + return buildGroup(&grp), nil +} + +func lookupGroupId(gid string) (*Group, error) { + i, e := strconv.Atoi(gid) + if e != nil { + return nil, e + } + return lookupUnixGid(i) +} + +func lookupUnixGid(gid int) (*Group, error) { + var grp _C_struct_group + var found bool + + err := retryWithBuffer(groupBuffer, func(buf []byte) syscall.Errno { + var errno syscall.Errno + grp, found, errno = _C_getgrgid_r(_C_gid_t(gid), + (*_C_char)(unsafe.Pointer(&buf[0])), _C_size_t(len(buf))) + return syscall.Errno(errno) + }) + if err != nil { + return nil, fmt.Errorf("user: lookup groupid %d: %v", gid, err) + } + if !found { + return nil, UnknownGroupIdError(strconv.Itoa(gid)) + } + return buildGroup(&grp), nil +} + +func buildGroup(grp *_C_struct_group) *Group { + g := &Group{ + Gid: strconv.Itoa(int(_C_gr_gid(grp))), + Name: _C_GoString(_C_gr_name(grp)), + } + return g +} + +type bufferKind _C_int + +var ( + userBuffer = bufferKind(_C__SC_GETPW_R_SIZE_MAX) + groupBuffer = bufferKind(_C__SC_GETGR_R_SIZE_MAX) +) + +func (k bufferKind) initialSize() _C_size_t { + sz := _C_sysconf(_C_int(k)) + if sz == -1 { + // DragonFly and FreeBSD do not have _SC_GETPW_R_SIZE_MAX. + // Additionally, not all Linux systems have it, either. For + // example, the musl libc returns -1. + return 1024 + } + if !isSizeReasonable(int64(sz)) { + // Truncate. If this truly isn't enough, retryWithBuffer will error on the first run. + return maxBufferSize + } + return _C_size_t(sz) +} + +// retryWithBuffer repeatedly calls f(), increasing the size of the +// buffer each time, until f succeeds, fails with a non-ERANGE error, +// or the buffer exceeds a reasonable limit. +func retryWithBuffer(startSize bufferKind, f func([]byte) syscall.Errno) error { + buf := make([]byte, startSize) + for { + errno := f(buf) + if errno == 0 { + return nil + } else if runtime.GOOS == "aix" && errno+1 == 0 { + // On AIX getpwuid_r appears to return -1, + // not ERANGE, on buffer overflow. + } else if errno != syscall.ERANGE { + return errno + } + newSize := len(buf) * 2 + if !isSizeReasonable(int64(newSize)) { + return fmt.Errorf("internal buffer exceeds %d bytes", maxBufferSize) + } + buf = make([]byte, newSize) + } +} + +const maxBufferSize = 1 << 20 + +func isSizeReasonable(sz int64) bool { + return sz > 0 && sz <= maxBufferSize +} + +// Because we can't use cgo in tests: +func structPasswdForNegativeTest() _C_struct_passwd { + sp := _C_struct_passwd{} + *_C_pw_uidp(&sp) = 1<<32 - 2 + *_C_pw_gidp(&sp) = 1<<32 - 3 + return sp +} diff --git a/src/os/user/cgo_unix_test.go b/src/os/user/cgo_unix_test.go new file mode 100644 index 0000000..6d16aa2 --- /dev/null +++ b/src/os/user/cgo_unix_test.go @@ -0,0 +1,23 @@ +// Copyright 2017 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. + +//go:build (darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris) && cgo && !osusergo + +package user + +import ( + "testing" +) + +// Issue 22739 +func TestNegativeUid(t *testing.T) { + sp := structPasswdForNegativeTest() + u := buildUser(&sp) + if g, w := u.Uid, "4294967294"; g != w { + t.Errorf("Uid = %q; want %q", g, w) + } + if g, w := u.Gid, "4294967293"; g != w { + t.Errorf("Gid = %q; want %q", g, w) + } +} diff --git a/src/os/user/cgo_user_test.go b/src/os/user/cgo_user_test.go new file mode 100644 index 0000000..0458495 --- /dev/null +++ b/src/os/user/cgo_user_test.go @@ -0,0 +1,11 @@ +// 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. + +//go:build cgo && !osusergo + +package user + +func init() { + hasCgo = true +} diff --git a/src/os/user/getgrouplist_syscall.go b/src/os/user/getgrouplist_syscall.go new file mode 100644 index 0000000..41b64fc --- /dev/null +++ b/src/os/user/getgrouplist_syscall.go @@ -0,0 +1,19 @@ +// Copyright 2016 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. + +//go:build !osusergo && darwin + +package user + +import ( + "internal/syscall/unix" +) + +func getGroupList(name *_C_char, userGID _C_gid_t, gids *_C_gid_t, n *_C_int) _C_int { + err := unix.Getgrouplist(name, userGID, gids, n) + if err != nil { + return -1 + } + return 0 +} diff --git a/src/os/user/getgrouplist_unix.go b/src/os/user/getgrouplist_unix.go new file mode 100644 index 0000000..fb482d3 --- /dev/null +++ b/src/os/user/getgrouplist_unix.go @@ -0,0 +1,22 @@ +// Copyright 2016 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. + +//go:build cgo && !osusergo && (dragonfly || freebsd || (!android && linux) || netbsd || openbsd || (solaris && !illumos)) + +package user + +/* +#include +#include +#include + +static int mygetgrouplist(const char* user, gid_t group, gid_t* groups, int* ngroups) { + return getgrouplist(user, group, groups, ngroups); +} +*/ +import "C" + +func getGroupList(name *_C_char, userGID _C_gid_t, gids *_C_gid_t, n *_C_int) _C_int { + return C.mygetgrouplist(name, userGID, gids, n) +} diff --git a/src/os/user/listgroups_stub.go b/src/os/user/listgroups_stub.go new file mode 100644 index 0000000..aa7df93 --- /dev/null +++ b/src/os/user/listgroups_stub.go @@ -0,0 +1,19 @@ +// Copyright 2021 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. + +//go:build android + +package user + +import ( + "errors" +) + +func init() { + groupListImplemented = false +} + +func listGroups(*User) ([]string, error) { + return nil, errors.New("user: list groups not implemented") +} diff --git a/src/os/user/listgroups_unix.go b/src/os/user/listgroups_unix.go new file mode 100644 index 0000000..67bd8a7 --- /dev/null +++ b/src/os/user/listgroups_unix.go @@ -0,0 +1,109 @@ +// Copyright 2021 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. + +//go:build ((darwin || dragonfly || freebsd || (js && wasm) || wasip1 || (!android && linux) || netbsd || openbsd || solaris) && ((!cgo && !darwin) || osusergo)) || aix || illumos + +package user + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "os" + "strconv" +) + +func listGroupsFromReader(u *User, r io.Reader) ([]string, error) { + if u.Username == "" { + return nil, errors.New("user: list groups: empty username") + } + primaryGid, err := strconv.Atoi(u.Gid) + if err != nil { + return nil, fmt.Errorf("user: list groups for %s: invalid gid %q", u.Username, u.Gid) + } + + userCommas := []byte("," + u.Username + ",") // ,john, + userFirst := userCommas[1:] // john, + userLast := userCommas[:len(userCommas)-1] // ,john + userOnly := userCommas[1 : len(userCommas)-1] // john + + // Add primary Gid first. + groups := []string{u.Gid} + + rd := bufio.NewReader(r) + done := false + for !done { + line, err := rd.ReadBytes('\n') + if err != nil { + if err == io.EOF { + done = true + } else { + return groups, err + } + } + + // Look for username in the list of users. If user is found, + // append the GID to the groups slice. + + // There's no spec for /etc/passwd or /etc/group, but we try to follow + // the same rules as the glibc parser, which allows comments and blank + // space at the beginning of a line. + line = bytes.TrimSpace(line) + if len(line) == 0 || line[0] == '#' || + // If you search for a gid in a row where the group + // name (the first field) starts with "+" or "-", + // glibc fails to find the record, and so should we. + line[0] == '+' || line[0] == '-' { + continue + } + + // Format of /etc/group is + // groupname:password:GID:user_list + // for example + // wheel:x:10:john,paul,jack + // tcpdump:x:72: + listIdx := bytes.LastIndexByte(line, ':') + if listIdx == -1 || listIdx == len(line)-1 { + // No commas, or empty group list. + continue + } + if bytes.Count(line[:listIdx], colon) != 2 { + // Incorrect number of colons. + continue + } + list := line[listIdx+1:] + // Check the list for user without splitting or copying. + if !(bytes.Equal(list, userOnly) || bytes.HasPrefix(list, userFirst) || bytes.HasSuffix(list, userLast) || bytes.Contains(list, userCommas)) { + continue + } + + // groupname:password:GID + parts := bytes.Split(line[:listIdx], colon) + if len(parts) != 3 || len(parts[0]) == 0 { + continue + } + gid := string(parts[2]) + // Make sure it's numeric and not the same as primary GID. + numGid, err := strconv.Atoi(gid) + if err != nil || numGid == primaryGid { + continue + } + + groups = append(groups, gid) + } + + return groups, nil +} + +func listGroups(u *User) ([]string, error) { + f, err := os.Open(groupFile) + if err != nil { + return nil, err + } + defer f.Close() + + return listGroupsFromReader(u, f) +} diff --git a/src/os/user/listgroups_unix_test.go b/src/os/user/listgroups_unix_test.go new file mode 100644 index 0000000..ae50319 --- /dev/null +++ b/src/os/user/listgroups_unix_test.go @@ -0,0 +1,107 @@ +// Copyright 2021 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. + +//go:build ((darwin || dragonfly || freebsd || (js && wasm) || wasip1 || (!android && linux) || netbsd || openbsd || solaris) && ((!cgo && !darwin) || osusergo)) || aix || illumos + +package user + +import ( + "fmt" + "sort" + "strings" + "testing" +) + +var testGroupFile = `# See the opendirectoryd(8) man page for additional +# information about Open Directory. +## +nobody:*:-2: +nogroup:*:-1: +wheel:*:0:root +emptyid:*::root +invalidgid:*:notanumber:root ++plussign:*:20:root +-minussign:*:21:root +# Next line is invalid (empty group name) +:*:22:root + +daemon:*:1:root + indented:*:7:root +# comment:*:4:found + # comment:*:4:found +kmem:*:2:root +manymembers:x:777:jill,jody,john,jack,jov,user777 +` + largeGroup() + +func largeGroup() (res string) { + var b strings.Builder + b.WriteString("largegroup:x:1000:user1") + for i := 2; i <= 7500; i++ { + fmt.Fprintf(&b, ",user%d", i) + } + return b.String() +} + +var listGroupsTests = []struct { + // input + in string + user string + gid string + // output + gids []string + err bool +}{ + {in: testGroupFile, user: "root", gid: "0", gids: []string{"0", "1", "2", "7"}}, + {in: testGroupFile, user: "jill", gid: "33", gids: []string{"33", "777"}}, + {in: testGroupFile, user: "jody", gid: "34", gids: []string{"34", "777"}}, + {in: testGroupFile, user: "john", gid: "35", gids: []string{"35", "777"}}, + {in: testGroupFile, user: "jov", gid: "37", gids: []string{"37", "777"}}, + {in: testGroupFile, user: "user777", gid: "7", gids: []string{"7", "777", "1000"}}, + {in: testGroupFile, user: "user1111", gid: "1111", gids: []string{"1111", "1000"}}, + {in: testGroupFile, user: "user1000", gid: "1000", gids: []string{"1000"}}, + {in: testGroupFile, user: "user7500", gid: "7500", gids: []string{"1000", "7500"}}, + {in: testGroupFile, user: "no-such-user", gid: "2345", gids: []string{"2345"}}, + {in: "", user: "no-such-user", gid: "2345", gids: []string{"2345"}}, + // Error cases. + {in: "", user: "", gid: "2345", err: true}, + {in: "", user: "joanna", gid: "bad", err: true}, +} + +func TestListGroups(t *testing.T) { + for _, tc := range listGroupsTests { + u := &User{Username: tc.user, Gid: tc.gid} + got, err := listGroupsFromReader(u, strings.NewReader(tc.in)) + if tc.err { + if err == nil { + t.Errorf("listGroups(%q): got nil; want error", tc.user) + } + continue // no more checks + } + if err != nil { + t.Errorf("listGroups(%q): got %v error, want nil", tc.user, err) + continue // no more checks + } + checkSameIDs(t, got, tc.gids) + } +} + +func checkSameIDs(t *testing.T, got, want []string) { + t.Helper() + if len(got) != len(want) { + t.Errorf("ID list mismatch: got %v; want %v", got, want) + return + } + sort.Strings(got) + sort.Strings(want) + mismatch := -1 + for i, g := range want { + if got[i] != g { + mismatch = i + break + } + } + if mismatch != -1 { + t.Errorf("ID list mismatch (at index %d): got %v; want %v", mismatch, got, want) + } +} diff --git a/src/os/user/lookup.go b/src/os/user/lookup.go new file mode 100644 index 0000000..ed33d0c --- /dev/null +++ b/src/os/user/lookup.go @@ -0,0 +1,70 @@ +// Copyright 2011 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 user + +import "sync" + +const ( + userFile = "/etc/passwd" + groupFile = "/etc/group" +) + +var colon = []byte{':'} + +// Current returns the current user. +// +// The first call will cache the current user information. +// Subsequent calls will return the cached value and will not reflect +// changes to the current user. +func Current() (*User, error) { + cache.Do(func() { cache.u, cache.err = current() }) + if cache.err != nil { + return nil, cache.err + } + u := *cache.u // copy + return &u, nil +} + +// cache of the current user +var cache struct { + sync.Once + u *User + err error +} + +// Lookup looks up a user by username. If the user cannot be found, the +// returned error is of type UnknownUserError. +func Lookup(username string) (*User, error) { + if u, err := Current(); err == nil && u.Username == username { + return u, err + } + return lookupUser(username) +} + +// LookupId looks up a user by userid. If the user cannot be found, the +// returned error is of type UnknownUserIdError. +func LookupId(uid string) (*User, error) { + if u, err := Current(); err == nil && u.Uid == uid { + return u, err + } + return lookupUserId(uid) +} + +// LookupGroup looks up a group by name. If the group cannot be found, the +// returned error is of type UnknownGroupError. +func LookupGroup(name string) (*Group, error) { + return lookupGroup(name) +} + +// LookupGroupId looks up a group by groupid. If the group cannot be found, the +// returned error is of type UnknownGroupIdError. +func LookupGroupId(gid string) (*Group, error) { + return lookupGroupId(gid) +} + +// GroupIds returns the list of group IDs that the user is a member of. +func (u *User) GroupIds() ([]string, error) { + return listGroups(u) +} diff --git a/src/os/user/lookup_android.go b/src/os/user/lookup_android.go new file mode 100644 index 0000000..0ae31fd --- /dev/null +++ b/src/os/user/lookup_android.go @@ -0,0 +1,25 @@ +// Copyright 2016 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. + +//go:build android + +package user + +import "errors" + +func lookupUser(string) (*User, error) { + return nil, errors.New("user: Lookup not implemented on android") +} + +func lookupUserId(string) (*User, error) { + return nil, errors.New("user: LookupId not implemented on android") +} + +func lookupGroup(string) (*Group, error) { + return nil, errors.New("user: LookupGroup not implemented on android") +} + +func lookupGroupId(string) (*Group, error) { + return nil, errors.New("user: LookupGroupId not implemented on android") +} diff --git a/src/os/user/lookup_plan9.go b/src/os/user/lookup_plan9.go new file mode 100644 index 0000000..c2aabd5 --- /dev/null +++ b/src/os/user/lookup_plan9.go @@ -0,0 +1,67 @@ +// Copyright 2013 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 user + +import ( + "fmt" + "os" + "syscall" +) + +// Partial os/user support on Plan 9. +// Supports Current(), but not Lookup()/LookupId(). +// The latter two would require parsing /adm/users. + +func init() { + userImplemented = false + groupImplemented = false + groupListImplemented = false +} + +var ( + // unused variables (in this implementation) + // modified during test to exercise code paths in the cgo implementation. + userBuffer = 0 + groupBuffer = 0 +) + +func current() (*User, error) { + ubytes, err := os.ReadFile("/dev/user") + if err != nil { + return nil, fmt.Errorf("user: %s", err) + } + + uname := string(ubytes) + + u := &User{ + Uid: uname, + Gid: uname, + Username: uname, + Name: uname, + HomeDir: os.Getenv("home"), + } + + return u, nil +} + +func lookupUser(username string) (*User, error) { + return nil, syscall.EPLAN9 +} + +func lookupUserId(uid string) (*User, error) { + return nil, syscall.EPLAN9 +} + +func lookupGroup(groupname string) (*Group, error) { + return nil, syscall.EPLAN9 +} + +func lookupGroupId(string) (*Group, error) { + return nil, syscall.EPLAN9 +} + +func listGroups(*User) ([]string, error) { + return nil, syscall.EPLAN9 +} diff --git a/src/os/user/lookup_stubs.go b/src/os/user/lookup_stubs.go new file mode 100644 index 0000000..89dfe45 --- /dev/null +++ b/src/os/user/lookup_stubs.go @@ -0,0 +1,83 @@ +// Copyright 2011 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. + +//go:build (!cgo && !darwin && !windows && !plan9) || android || (osusergo && !windows && !plan9) + +package user + +import ( + "fmt" + "os" + "runtime" + "strconv" +) + +var ( + // unused variables (in this implementation) + // modified during test to exercise code paths in the cgo implementation. + userBuffer = 0 + groupBuffer = 0 +) + +func current() (*User, error) { + uid := currentUID() + // $USER and /etc/passwd may disagree; prefer the latter if we can get it. + // See issue 27524 for more information. + u, err := lookupUserId(uid) + if err == nil { + return u, nil + } + + homeDir, _ := os.UserHomeDir() + u = &User{ + Uid: uid, + Gid: currentGID(), + Username: os.Getenv("USER"), + Name: "", // ignored + HomeDir: homeDir, + } + // On Android, return a dummy user instead of failing. + switch runtime.GOOS { + case "android": + if u.Uid == "" { + u.Uid = "1" + } + if u.Username == "" { + u.Username = "android" + } + } + // cgo isn't available, but if we found the minimum information + // without it, use it: + if u.Uid != "" && u.Username != "" && u.HomeDir != "" { + return u, nil + } + var missing string + if u.Username == "" { + missing = "$USER" + } + if u.HomeDir == "" { + if missing != "" { + missing += ", " + } + missing += "$HOME" + } + return u, fmt.Errorf("user: Current requires cgo or %s set in environment", missing) +} + +func currentUID() string { + if id := os.Getuid(); id >= 0 { + return strconv.Itoa(id) + } + // Note: Windows returns -1, but this file isn't used on + // Windows anyway, so this empty return path shouldn't be + // used. + return "" +} + +func currentGID() string { + if id := os.Getgid(); id >= 0 { + return strconv.Itoa(id) + } + return "" +} diff --git a/src/os/user/lookup_unix.go b/src/os/user/lookup_unix.go new file mode 100644 index 0000000..a430826 --- /dev/null +++ b/src/os/user/lookup_unix.go @@ -0,0 +1,234 @@ +// Copyright 2016 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. + +//go:build ((unix && !android) || (js && wasm) || wasip1) && ((!cgo && !darwin) || osusergo) + +package user + +import ( + "bufio" + "bytes" + "errors" + "io" + "os" + "strconv" + "strings" +) + +// lineFunc returns a value, an error, or (nil, nil) to skip the row. +type lineFunc func(line []byte) (v any, err error) + +// readColonFile parses r as an /etc/group or /etc/passwd style file, running +// fn for each row. readColonFile returns a value, an error, or (nil, nil) if +// the end of the file is reached without a match. +// +// readCols is the minimum number of colon-separated fields that will be passed +// to fn; in a long line additional fields may be silently discarded. +func readColonFile(r io.Reader, fn lineFunc, readCols int) (v any, err error) { + rd := bufio.NewReader(r) + + // Read the file line-by-line. + for { + var isPrefix bool + var wholeLine []byte + + // Read the next line. We do so in chunks (as much as reader's + // buffer is able to keep), check if we read enough columns + // already on each step and store final result in wholeLine. + for { + var line []byte + line, isPrefix, err = rd.ReadLine() + + if err != nil { + // We should return (nil, nil) if EOF is reached + // without a match. + if err == io.EOF { + err = nil + } + return nil, err + } + + // Simple common case: line is short enough to fit in a + // single reader's buffer. + if !isPrefix && len(wholeLine) == 0 { + wholeLine = line + break + } + + wholeLine = append(wholeLine, line...) + + // Check if we read the whole line (or enough columns) + // already. + if !isPrefix || bytes.Count(wholeLine, []byte{':'}) >= readCols { + break + } + } + + // There's no spec for /etc/passwd or /etc/group, but we try to follow + // the same rules as the glibc parser, which allows comments and blank + // space at the beginning of a line. + wholeLine = bytes.TrimSpace(wholeLine) + if len(wholeLine) == 0 || wholeLine[0] == '#' { + continue + } + v, err = fn(wholeLine) + if v != nil || err != nil { + return + } + + // If necessary, skip the rest of the line + for ; isPrefix; _, isPrefix, err = rd.ReadLine() { + if err != nil { + // We should return (nil, nil) if EOF is reached without a match. + if err == io.EOF { + err = nil + } + return nil, err + } + } + } +} + +func matchGroupIndexValue(value string, idx int) lineFunc { + var leadColon string + if idx > 0 { + leadColon = ":" + } + substr := []byte(leadColon + value + ":") + return func(line []byte) (v any, err error) { + if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 3 { + return + } + // wheel:*:0:root + parts := strings.SplitN(string(line), ":", 4) + if len(parts) < 4 || parts[0] == "" || parts[idx] != value || + // If the file contains +foo and you search for "foo", glibc + // returns an "invalid argument" error. Similarly, if you search + // for a gid for a row where the group name starts with "+" or "-", + // glibc fails to find the record. + parts[0][0] == '+' || parts[0][0] == '-' { + return + } + if _, err := strconv.Atoi(parts[2]); err != nil { + return nil, nil + } + return &Group{Name: parts[0], Gid: parts[2]}, nil + } +} + +func findGroupId(id string, r io.Reader) (*Group, error) { + if v, err := readColonFile(r, matchGroupIndexValue(id, 2), 3); err != nil { + return nil, err + } else if v != nil { + return v.(*Group), nil + } + return nil, UnknownGroupIdError(id) +} + +func findGroupName(name string, r io.Reader) (*Group, error) { + if v, err := readColonFile(r, matchGroupIndexValue(name, 0), 3); err != nil { + return nil, err + } else if v != nil { + return v.(*Group), nil + } + return nil, UnknownGroupError(name) +} + +// returns a *User for a row if that row's has the given value at the +// given index. +func matchUserIndexValue(value string, idx int) lineFunc { + var leadColon string + if idx > 0 { + leadColon = ":" + } + substr := []byte(leadColon + value + ":") + return func(line []byte) (v any, err error) { + if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 6 { + return + } + // kevin:x:1005:1006::/home/kevin:/usr/bin/zsh + parts := strings.SplitN(string(line), ":", 7) + if len(parts) < 6 || parts[idx] != value || parts[0] == "" || + parts[0][0] == '+' || parts[0][0] == '-' { + return + } + if _, err := strconv.Atoi(parts[2]); err != nil { + return nil, nil + } + if _, err := strconv.Atoi(parts[3]); err != nil { + return nil, nil + } + u := &User{ + Username: parts[0], + Uid: parts[2], + Gid: parts[3], + Name: parts[4], + HomeDir: parts[5], + } + // The pw_gecos field isn't quite standardized. Some docs + // say: "It is expected to be a comma separated list of + // personal data where the first item is the full name of the + // user." + u.Name, _, _ = strings.Cut(u.Name, ",") + return u, nil + } +} + +func findUserId(uid string, r io.Reader) (*User, error) { + i, e := strconv.Atoi(uid) + if e != nil { + return nil, errors.New("user: invalid userid " + uid) + } + if v, err := readColonFile(r, matchUserIndexValue(uid, 2), 6); err != nil { + return nil, err + } else if v != nil { + return v.(*User), nil + } + return nil, UnknownUserIdError(i) +} + +func findUsername(name string, r io.Reader) (*User, error) { + if v, err := readColonFile(r, matchUserIndexValue(name, 0), 6); err != nil { + return nil, err + } else if v != nil { + return v.(*User), nil + } + return nil, UnknownUserError(name) +} + +func lookupGroup(groupname string) (*Group, error) { + f, err := os.Open(groupFile) + if err != nil { + return nil, err + } + defer f.Close() + return findGroupName(groupname, f) +} + +func lookupGroupId(id string) (*Group, error) { + f, err := os.Open(groupFile) + if err != nil { + return nil, err + } + defer f.Close() + return findGroupId(id, f) +} + +func lookupUser(username string) (*User, error) { + f, err := os.Open(userFile) + if err != nil { + return nil, err + } + defer f.Close() + return findUsername(username, f) +} + +func lookupUserId(uid string) (*User, error) { + f, err := os.Open(userFile) + if err != nil { + return nil, err + } + defer f.Close() + return findUserId(uid, f) +} diff --git a/src/os/user/lookup_unix_test.go b/src/os/user/lookup_unix_test.go new file mode 100644 index 0000000..78b3392 --- /dev/null +++ b/src/os/user/lookup_unix_test.go @@ -0,0 +1,259 @@ +// Copyright 2016 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. + +//go:build unix && !android && !cgo && !darwin + +package user + +import ( + "reflect" + "strings" + "testing" +) + +var groupTests = []struct { + in string + name string + gid string +}{ + {testGroupFile, "nobody", "-2"}, + {testGroupFile, "kmem", "2"}, + {testGroupFile, "notinthefile", ""}, + {testGroupFile, "comment", ""}, + {testGroupFile, "plussign", ""}, + {testGroupFile, "+plussign", ""}, + {testGroupFile, "-minussign", ""}, + {testGroupFile, "minussign", ""}, + {testGroupFile, "emptyid", ""}, + {testGroupFile, "invalidgid", ""}, + {testGroupFile, "indented", "7"}, + {testGroupFile, "# comment", ""}, + {testGroupFile, "largegroup", "1000"}, + {testGroupFile, "manymembers", "777"}, + {"", "emptyfile", ""}, +} + +func TestFindGroupName(t *testing.T) { + for _, tt := range groupTests { + got, err := findGroupName(tt.name, strings.NewReader(tt.in)) + if tt.gid == "" { + if err == nil { + t.Errorf("findGroupName(%s): got nil error, expected err", tt.name) + continue + } + switch terr := err.(type) { + case UnknownGroupError: + if terr.Error() != "group: unknown group "+tt.name { + t.Errorf("findGroupName(%s): got %v, want %v", tt.name, terr, tt.name) + } + default: + t.Errorf("findGroupName(%s): got unexpected error %v", tt.name, terr) + } + } else { + if err != nil { + t.Fatalf("findGroupName(%s): got unexpected error %v", tt.name, err) + } + if got.Gid != tt.gid { + t.Errorf("findGroupName(%s): got gid %v, want %s", tt.name, got.Gid, tt.gid) + } + if got.Name != tt.name { + t.Errorf("findGroupName(%s): got name %s, want %s", tt.name, got.Name, tt.name) + } + } + } +} + +var groupIdTests = []struct { + in string + gid string + name string +}{ + {testGroupFile, "-2", "nobody"}, + {testGroupFile, "2", "kmem"}, + {testGroupFile, "notinthefile", ""}, + {testGroupFile, "comment", ""}, + {testGroupFile, "7", "indented"}, + {testGroupFile, "4", ""}, + {testGroupFile, "20", ""}, // row starts with a plus + {testGroupFile, "21", ""}, // row starts with a minus + {"", "emptyfile", ""}, +} + +func TestFindGroupId(t *testing.T) { + for _, tt := range groupIdTests { + got, err := findGroupId(tt.gid, strings.NewReader(tt.in)) + if tt.name == "" { + if err == nil { + t.Errorf("findGroupId(%s): got nil error, expected err", tt.gid) + continue + } + switch terr := err.(type) { + case UnknownGroupIdError: + if terr.Error() != "group: unknown groupid "+tt.gid { + t.Errorf("findGroupId(%s): got %v, want %v", tt.name, terr, tt.name) + } + default: + t.Errorf("findGroupId(%s): got unexpected error %v", tt.name, terr) + } + } else { + if err != nil { + t.Fatalf("findGroupId(%s): got unexpected error %v", tt.name, err) + } + if got.Gid != tt.gid { + t.Errorf("findGroupId(%s): got gid %v, want %s", tt.name, got.Gid, tt.gid) + } + if got.Name != tt.name { + t.Errorf("findGroupId(%s): got name %s, want %s", tt.name, got.Name, tt.name) + } + } + } +} + +const testUserFile = ` # Example user file +root:x:0:0:root:/root:/bin/bash +daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin +bin:x:2:3:bin:/bin:/usr/sbin/nologin + indented:x:3:3:indented:/dev:/usr/sbin/nologin +sync:x:4:65534:sync:/bin:/bin/sync +negative:x:-5:60:games:/usr/games:/usr/sbin/nologin +man:x:6:12:man:/var/cache/man:/usr/sbin/nologin +allfields:x:6:12:mansplit,man2,man3,man4:/home/allfields:/usr/sbin/nologin ++plussign:x:8:10:man:/var/cache/man:/usr/sbin/nologin +-minussign:x:9:10:man:/var/cache/man:/usr/sbin/nologin + +malformed:x:27:12 # more:colons:after:comment + +struid:x:notanumber:12 # more:colons:after:comment + +# commented:x:28:12:commented:/var/cache/man:/usr/sbin/nologin + # commentindented:x:29:12:commentindented:/var/cache/man:/usr/sbin/nologin + +struid2:x:30:badgid:struid2name:/home/struid:/usr/sbin/nologin +` + +var userIdTests = []struct { + in string + uid string + name string +}{ + {testUserFile, "-5", "negative"}, + {testUserFile, "2", "bin"}, + {testUserFile, "100", ""}, // not in the file + {testUserFile, "8", ""}, // plus sign, glibc doesn't find it + {testUserFile, "9", ""}, // minus sign, glibc doesn't find it + {testUserFile, "27", ""}, // malformed + {testUserFile, "28", ""}, // commented out + {testUserFile, "29", ""}, // commented out, indented + {testUserFile, "3", "indented"}, + {testUserFile, "30", ""}, // the Gid is not valid, shouldn't match + {"", "1", ""}, +} + +func TestInvalidUserId(t *testing.T) { + _, err := findUserId("notanumber", strings.NewReader("")) + if err == nil { + t.Fatalf("findUserId('notanumber'): got nil error") + } + if want := "user: invalid userid notanumber"; err.Error() != want { + t.Errorf("findUserId('notanumber'): got %v, want %s", err, want) + } +} + +func TestLookupUserId(t *testing.T) { + for _, tt := range userIdTests { + got, err := findUserId(tt.uid, strings.NewReader(tt.in)) + if tt.name == "" { + if err == nil { + t.Errorf("findUserId(%s): got nil error, expected err", tt.uid) + continue + } + switch terr := err.(type) { + case UnknownUserIdError: + if want := "user: unknown userid " + tt.uid; terr.Error() != want { + t.Errorf("findUserId(%s): got %v, want %v", tt.name, terr, want) + } + default: + t.Errorf("findUserId(%s): got unexpected error %v", tt.name, terr) + } + } else { + if err != nil { + t.Fatalf("findUserId(%s): got unexpected error %v", tt.name, err) + } + if got.Uid != tt.uid { + t.Errorf("findUserId(%s): got uid %v, want %s", tt.name, got.Uid, tt.uid) + } + if got.Username != tt.name { + t.Errorf("findUserId(%s): got name %s, want %s", tt.name, got.Username, tt.name) + } + } + } +} + +func TestLookupUserPopulatesAllFields(t *testing.T) { + u, err := findUsername("allfields", strings.NewReader(testUserFile)) + if err != nil { + t.Fatal(err) + } + want := &User{ + Username: "allfields", + Uid: "6", + Gid: "12", + Name: "mansplit", + HomeDir: "/home/allfields", + } + if !reflect.DeepEqual(u, want) { + t.Errorf("findUsername: got %#v, want %#v", u, want) + } +} + +var userTests = []struct { + in string + name string + uid string +}{ + {testUserFile, "negative", "-5"}, + {testUserFile, "bin", "2"}, + {testUserFile, "notinthefile", ""}, + {testUserFile, "indented", "3"}, + {testUserFile, "plussign", ""}, + {testUserFile, "+plussign", ""}, + {testUserFile, "minussign", ""}, + {testUserFile, "-minussign", ""}, + {testUserFile, " indented", ""}, + {testUserFile, "commented", ""}, + {testUserFile, "commentindented", ""}, + {testUserFile, "malformed", ""}, + {testUserFile, "# commented", ""}, + {"", "emptyfile", ""}, +} + +func TestLookupUser(t *testing.T) { + for _, tt := range userTests { + got, err := findUsername(tt.name, strings.NewReader(tt.in)) + if tt.uid == "" { + if err == nil { + t.Errorf("lookupUser(%s): got nil error, expected err", tt.uid) + continue + } + switch terr := err.(type) { + case UnknownUserError: + if want := "user: unknown user " + tt.name; terr.Error() != want { + t.Errorf("lookupUser(%s): got %v, want %v", tt.name, terr, want) + } + default: + t.Errorf("lookupUser(%s): got unexpected error %v", tt.name, terr) + } + } else { + if err != nil { + t.Fatalf("lookupUser(%s): got unexpected error %v", tt.name, err) + } + if got.Uid != tt.uid { + t.Errorf("lookupUser(%s): got uid %v, want %s", tt.name, got.Uid, tt.uid) + } + if got.Username != tt.name { + t.Errorf("lookupUser(%s): got name %s, want %s", tt.name, got.Username, tt.name) + } + } + } +} diff --git a/src/os/user/lookup_windows.go b/src/os/user/lookup_windows.go new file mode 100644 index 0000000..e64b8ae --- /dev/null +++ b/src/os/user/lookup_windows.go @@ -0,0 +1,392 @@ +// Copyright 2012 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 user + +import ( + "fmt" + "internal/syscall/windows" + "internal/syscall/windows/registry" + "syscall" + "unsafe" +) + +func isDomainJoined() (bool, error) { + var domain *uint16 + var status uint32 + err := syscall.NetGetJoinInformation(nil, &domain, &status) + if err != nil { + return false, err + } + syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain))) + return status == syscall.NetSetupDomainName, nil +} + +func lookupFullNameDomain(domainAndUser string) (string, error) { + return syscall.TranslateAccountName(domainAndUser, + syscall.NameSamCompatible, syscall.NameDisplay, 50) +} + +func lookupFullNameServer(servername, username string) (string, error) { + s, e := syscall.UTF16PtrFromString(servername) + if e != nil { + return "", e + } + u, e := syscall.UTF16PtrFromString(username) + if e != nil { + return "", e + } + var p *byte + e = syscall.NetUserGetInfo(s, u, 10, &p) + if e != nil { + return "", e + } + defer syscall.NetApiBufferFree(p) + i := (*syscall.UserInfo10)(unsafe.Pointer(p)) + return windows.UTF16PtrToString(i.FullName), nil +} + +func lookupFullName(domain, username, domainAndUser string) (string, error) { + joined, err := isDomainJoined() + if err == nil && joined { + name, err := lookupFullNameDomain(domainAndUser) + if err == nil { + return name, nil + } + } + name, err := lookupFullNameServer(domain, username) + if err == nil { + return name, nil + } + // domain worked neither as a domain nor as a server + // could be domain server unavailable + // pretend username is fullname + return username, nil +} + +// getProfilesDirectory retrieves the path to the root directory +// where user profiles are stored. +func getProfilesDirectory() (string, error) { + n := uint32(100) + for { + b := make([]uint16, n) + e := windows.GetProfilesDirectory(&b[0], &n) + if e == nil { + return syscall.UTF16ToString(b), nil + } + if e != syscall.ERROR_INSUFFICIENT_BUFFER { + return "", e + } + if n <= uint32(len(b)) { + return "", e + } + } +} + +// lookupUsernameAndDomain obtains the username and domain for usid. +func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, e error) { + username, domain, t, e := usid.LookupAccount("") + if e != nil { + return "", "", e + } + if t != syscall.SidTypeUser { + return "", "", fmt.Errorf("user: should be user account type, not %d", t) + } + return username, domain, nil +} + +// findHomeDirInRegistry finds the user home path based on the uid. +func findHomeDirInRegistry(uid string) (dir string, e error) { + k, e := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\`+uid, registry.QUERY_VALUE) + if e != nil { + return "", e + } + defer k.Close() + dir, _, e = k.GetStringValue("ProfileImagePath") + if e != nil { + return "", e + } + return dir, nil +} + +// lookupGroupName accepts the name of a group and retrieves the group SID. +func lookupGroupName(groupname string) (string, error) { + sid, _, t, e := syscall.LookupSID("", groupname) + if e != nil { + return "", e + } + // https://msdn.microsoft.com/en-us/library/cc245478.aspx#gt_0387e636-5654-4910-9519-1f8326cf5ec0 + // SidTypeAlias should also be treated as a group type next to SidTypeGroup + // and SidTypeWellKnownGroup: + // "alias object -> resource group: A group object..." + // + // Tests show that "Administrators" can be considered of type SidTypeAlias. + if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias { + return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t) + } + return sid.String() +} + +// listGroupsForUsernameAndDomain accepts username and domain and retrieves +// a SID list of the local groups where this user is a member. +func listGroupsForUsernameAndDomain(username, domain string) ([]string, error) { + // Check if both the domain name and user should be used. + var query string + joined, err := isDomainJoined() + if err == nil && joined && len(domain) != 0 { + query = domain + `\` + username + } else { + query = username + } + q, err := syscall.UTF16PtrFromString(query) + if err != nil { + return nil, err + } + var p0 *byte + var entriesRead, totalEntries uint32 + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa370655(v=vs.85).aspx + // NetUserGetLocalGroups() would return a list of LocalGroupUserInfo0 + // elements which hold the names of local groups where the user participates. + // The list does not follow any sorting order. + // + // If no groups can be found for this user, NetUserGetLocalGroups() should + // always return the SID of a single group called "None", which + // also happens to be the primary group for the local user. + err = windows.NetUserGetLocalGroups(nil, q, 0, windows.LG_INCLUDE_INDIRECT, &p0, windows.MAX_PREFERRED_LENGTH, &entriesRead, &totalEntries) + if err != nil { + return nil, err + } + defer syscall.NetApiBufferFree(p0) + if entriesRead == 0 { + return nil, fmt.Errorf("listGroupsForUsernameAndDomain: NetUserGetLocalGroups() returned an empty list for domain: %s, username: %s", domain, username) + } + entries := (*[1024]windows.LocalGroupUserInfo0)(unsafe.Pointer(p0))[:entriesRead:entriesRead] + var sids []string + for _, entry := range entries { + if entry.Name == nil { + continue + } + sid, err := lookupGroupName(windows.UTF16PtrToString(entry.Name)) + if err != nil { + return nil, err + } + sids = append(sids, sid) + } + return sids, nil +} + +func newUser(uid, gid, dir, username, domain string) (*User, error) { + domainAndUser := domain + `\` + username + name, e := lookupFullName(domain, username, domainAndUser) + if e != nil { + return nil, e + } + u := &User{ + Uid: uid, + Gid: gid, + Username: domainAndUser, + Name: name, + HomeDir: dir, + } + return u, nil +} + +var ( + // unused variables (in this implementation) + // modified during test to exercise code paths in the cgo implementation. + userBuffer = 0 + groupBuffer = 0 +) + +func current() (*User, error) { + t, e := syscall.OpenCurrentProcessToken() + if e != nil { + return nil, e + } + defer t.Close() + u, e := t.GetTokenUser() + if e != nil { + return nil, e + } + pg, e := t.GetTokenPrimaryGroup() + if e != nil { + return nil, e + } + uid, e := u.User.Sid.String() + if e != nil { + return nil, e + } + gid, e := pg.PrimaryGroup.String() + if e != nil { + return nil, e + } + dir, e := t.GetUserProfileDirectory() + if e != nil { + return nil, e + } + username, domain, e := lookupUsernameAndDomain(u.User.Sid) + if e != nil { + return nil, e + } + return newUser(uid, gid, dir, username, domain) +} + +// lookupUserPrimaryGroup obtains the primary group SID for a user using this method: +// https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for +// The method follows this formula: domainRID + "-" + primaryGroupRID +func lookupUserPrimaryGroup(username, domain string) (string, error) { + // get the domain RID + sid, _, t, e := syscall.LookupSID("", domain) + if e != nil { + return "", e + } + if t != syscall.SidTypeDomain { + return "", fmt.Errorf("lookupUserPrimaryGroup: should be domain account type, not %d", t) + } + domainRID, e := sid.String() + if e != nil { + return "", e + } + // If the user has joined a domain use the RID of the default primary group + // called "Domain Users": + // https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems + // SID: S-1-5-21domain-513 + // + // The correct way to obtain the primary group of a domain user is + // probing the user primaryGroupID attribute in the server Active Directory: + // https://msdn.microsoft.com/en-us/library/ms679375(v=vs.85).aspx + // + // Note that the primary group of domain users should not be modified + // on Windows for performance reasons, even if it's possible to do that. + // The .NET Developer's Guide to Directory Services Programming - Page 409 + // https://books.google.bg/books?id=kGApqjobEfsC&lpg=PA410&ots=p7oo-eOQL7&dq=primary%20group%20RID&hl=bg&pg=PA409#v=onepage&q&f=false + joined, err := isDomainJoined() + if err == nil && joined { + return domainRID + "-513", nil + } + // For non-domain users call NetUserGetInfo() with level 4, which + // in this case would not have any network overhead. + // The primary group should not change from RID 513 here either + // but the group will be called "None" instead: + // https://www.adampalmer.me/iodigitalsec/2013/08/10/windows-null-session-enumeration/ + // "Group 'None' (RID: 513)" + u, e := syscall.UTF16PtrFromString(username) + if e != nil { + return "", e + } + d, e := syscall.UTF16PtrFromString(domain) + if e != nil { + return "", e + } + var p *byte + e = syscall.NetUserGetInfo(d, u, 4, &p) + if e != nil { + return "", e + } + defer syscall.NetApiBufferFree(p) + i := (*windows.UserInfo4)(unsafe.Pointer(p)) + return fmt.Sprintf("%s-%d", domainRID, i.PrimaryGroupID), nil +} + +func newUserFromSid(usid *syscall.SID) (*User, error) { + username, domain, e := lookupUsernameAndDomain(usid) + if e != nil { + return nil, e + } + gid, e := lookupUserPrimaryGroup(username, domain) + if e != nil { + return nil, e + } + uid, e := usid.String() + if e != nil { + return nil, e + } + // If this user has logged in at least once their home path should be stored + // in the registry under the specified SID. References: + // https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx + // https://support.asperasoft.com/hc/en-us/articles/216127438-How-to-delete-Windows-user-profiles + // + // The registry is the most reliable way to find the home path as the user + // might have decided to move it outside of the default location, + // (e.g. C:\users). Reference: + // https://answers.microsoft.com/en-us/windows/forum/windows_7-security/how-do-i-set-a-home-directory-outside-cusers-for-a/aed68262-1bf4-4a4d-93dc-7495193a440f + dir, e := findHomeDirInRegistry(uid) + if e != nil { + // If the home path does not exist in the registry, the user might + // have not logged in yet; fall back to using getProfilesDirectory(). + // Find the username based on a SID and append that to the result of + // getProfilesDirectory(). The domain is not relevant here. + dir, e = getProfilesDirectory() + if e != nil { + return nil, e + } + dir += `\` + username + } + return newUser(uid, gid, dir, username, domain) +} + +func lookupUser(username string) (*User, error) { + sid, _, t, e := syscall.LookupSID("", username) + if e != nil { + return nil, e + } + if t != syscall.SidTypeUser { + return nil, fmt.Errorf("user: should be user account type, not %d", t) + } + return newUserFromSid(sid) +} + +func lookupUserId(uid string) (*User, error) { + sid, e := syscall.StringToSid(uid) + if e != nil { + return nil, e + } + return newUserFromSid(sid) +} + +func lookupGroup(groupname string) (*Group, error) { + sid, err := lookupGroupName(groupname) + if err != nil { + return nil, err + } + return &Group{Name: groupname, Gid: sid}, nil +} + +func lookupGroupId(gid string) (*Group, error) { + sid, err := syscall.StringToSid(gid) + if err != nil { + return nil, err + } + groupname, _, t, err := sid.LookupAccount("") + if err != nil { + return nil, err + } + if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias { + return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t) + } + return &Group{Name: groupname, Gid: gid}, nil +} + +func listGroups(user *User) ([]string, error) { + sid, err := syscall.StringToSid(user.Uid) + if err != nil { + return nil, err + } + username, domain, err := lookupUsernameAndDomain(sid) + if err != nil { + return nil, err + } + sids, err := listGroupsForUsernameAndDomain(username, domain) + if err != nil { + return nil, err + } + // Add the primary group of the user to the list if it is not already there. + // This is done only to comply with the POSIX concept of a primary group. + for _, sid := range sids { + if sid == user.Gid { + return sids, nil + } + } + return append(sids, user.Gid), nil +} diff --git a/src/os/user/user.go b/src/os/user/user.go new file mode 100644 index 0000000..0307d2a --- /dev/null +++ b/src/os/user/user.go @@ -0,0 +1,95 @@ +// Copyright 2011 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 user allows user account lookups by name or id. + +For most Unix systems, this package has two internal implementations of +resolving user and group ids to names, and listing supplementary group IDs. +One is written in pure Go and parses /etc/passwd and /etc/group. The other +is cgo-based and relies on the standard C library (libc) routines such as +getpwuid_r, getgrnam_r, and getgrouplist. + +When cgo is available, and the required routines are implemented in libc +for a particular platform, cgo-based (libc-backed) code is used. +This can be overridden by using osusergo build tag, which enforces +the pure Go implementation. +*/ +package user + +import ( + "strconv" +) + +// These may be set to false in init() for a particular platform and/or +// build flags to let the tests know to skip tests of some features. +var ( + userImplemented = true + groupImplemented = true + groupListImplemented = true +) + +// User represents a user account. +type User struct { + // Uid is the user ID. + // On POSIX systems, this is a decimal number representing the uid. + // On Windows, this is a security identifier (SID) in a string format. + // On Plan 9, this is the contents of /dev/user. + Uid string + // Gid is the primary group ID. + // On POSIX systems, this is a decimal number representing the gid. + // On Windows, this is a SID in a string format. + // On Plan 9, this is the contents of /dev/user. + Gid string + // Username is the login name. + Username string + // Name is the user's real or display name. + // It might be blank. + // On POSIX systems, this is the first (or only) entry in the GECOS field + // list. + // On Windows, this is the user's display name. + // On Plan 9, this is the contents of /dev/user. + Name string + // HomeDir is the path to the user's home directory (if they have one). + HomeDir string +} + +// Group represents a grouping of users. +// +// On POSIX systems Gid contains a decimal number representing the group ID. +type Group struct { + Gid string // group ID + Name string // group name +} + +// UnknownUserIdError is returned by LookupId when a user cannot be found. +type UnknownUserIdError int + +func (e UnknownUserIdError) Error() string { + return "user: unknown userid " + strconv.Itoa(int(e)) +} + +// UnknownUserError is returned by Lookup when +// a user cannot be found. +type UnknownUserError string + +func (e UnknownUserError) Error() string { + return "user: unknown user " + string(e) +} + +// UnknownGroupIdError is returned by LookupGroupId when +// a group cannot be found. +type UnknownGroupIdError string + +func (e UnknownGroupIdError) Error() string { + return "group: unknown groupid " + string(e) +} + +// UnknownGroupError is returned by LookupGroup when +// a group cannot be found. +type UnknownGroupError string + +func (e UnknownGroupError) Error() string { + return "group: unknown group " + string(e) +} diff --git a/src/os/user/user_test.go b/src/os/user/user_test.go new file mode 100644 index 0000000..fa597b7 --- /dev/null +++ b/src/os/user/user_test.go @@ -0,0 +1,192 @@ +// Copyright 2011 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 user + +import ( + "os" + "testing" +) + +var ( + hasCgo = false + hasUSER = os.Getenv("USER") != "" + hasHOME = os.Getenv("HOME") != "" +) + +func checkUser(t *testing.T) { + t.Helper() + if !userImplemented { + t.Skip("user: not implemented; skipping tests") + } +} + +func TestCurrent(t *testing.T) { + old := userBuffer + defer func() { + userBuffer = old + }() + userBuffer = 1 // force use of retry code + u, err := Current() + if err != nil { + if hasCgo || (hasUSER && hasHOME) { + t.Fatalf("Current: %v (got %#v)", err, u) + } else { + t.Skipf("skipping: %v", err) + } + } + if u.HomeDir == "" { + t.Errorf("didn't get a HomeDir") + } + if u.Username == "" { + t.Errorf("didn't get a username") + } +} + +func BenchmarkCurrent(b *testing.B) { + for i := 0; i < b.N; i++ { + Current() + } +} + +func compare(t *testing.T, want, got *User) { + if want.Uid != got.Uid { + t.Errorf("got Uid=%q; want %q", got.Uid, want.Uid) + } + if want.Username != got.Username { + t.Errorf("got Username=%q; want %q", got.Username, want.Username) + } + if want.Name != got.Name { + t.Errorf("got Name=%q; want %q", got.Name, want.Name) + } + if want.HomeDir != got.HomeDir { + t.Errorf("got HomeDir=%q; want %q", got.HomeDir, want.HomeDir) + } + if want.Gid != got.Gid { + t.Errorf("got Gid=%q; want %q", got.Gid, want.Gid) + } +} + +func TestLookup(t *testing.T) { + checkUser(t) + + want, err := Current() + if err != nil { + if hasCgo || (hasUSER && hasHOME) { + t.Fatalf("Current: %v", err) + } else { + t.Skipf("skipping: %v", err) + } + } + + // TODO: Lookup() has a fast path that calls Current() and returns if the + // usernames match, so this test does not exercise very much. It would be + // good to try and test finding a different user than the current user. + got, err := Lookup(want.Username) + if err != nil { + t.Fatalf("Lookup: %v", err) + } + compare(t, want, got) +} + +func TestLookupId(t *testing.T) { + checkUser(t) + + want, err := Current() + if err != nil { + if hasCgo || (hasUSER && hasHOME) { + t.Fatalf("Current: %v", err) + } else { + t.Skipf("skipping: %v", err) + } + } + + got, err := LookupId(want.Uid) + if err != nil { + t.Fatalf("LookupId: %v", err) + } + compare(t, want, got) +} + +func checkGroup(t *testing.T) { + t.Helper() + if !groupImplemented { + t.Skip("user: group not implemented; skipping test") + } +} + +func TestLookupGroup(t *testing.T) { + old := groupBuffer + defer func() { + groupBuffer = old + }() + groupBuffer = 1 // force use of retry code + checkGroup(t) + + user, err := Current() + if err != nil { + if hasCgo || (hasUSER && hasHOME) { + t.Fatalf("Current: %v", err) + } else { + t.Skipf("skipping: %v", err) + } + } + + g1, err := LookupGroupId(user.Gid) + if err != nil { + // NOTE(rsc): Maybe the group isn't defined. That's fine. + // On my OS X laptop, rsc logs in with group 5000 even + // though there's no name for group 5000. Such is Unix. + t.Logf("LookupGroupId(%q): %v", user.Gid, err) + return + } + if g1.Gid != user.Gid { + t.Errorf("LookupGroupId(%q).Gid = %s; want %s", user.Gid, g1.Gid, user.Gid) + } + + g2, err := LookupGroup(g1.Name) + if err != nil { + t.Fatalf("LookupGroup(%q): %v", g1.Name, err) + } + if g1.Gid != g2.Gid || g1.Name != g2.Name { + t.Errorf("LookupGroup(%q) = %+v; want %+v", g1.Name, g2, g1) + } +} + +func checkGroupList(t *testing.T) { + t.Helper() + if !groupListImplemented { + t.Skip("user: group list not implemented; skipping test") + } +} + +func TestGroupIds(t *testing.T) { + checkGroupList(t) + + user, err := Current() + if err != nil { + if hasCgo || (hasUSER && hasHOME) { + t.Fatalf("Current: %v", err) + } else { + t.Skipf("skipping: %v", err) + } + } + + gids, err := user.GroupIds() + if err != nil { + t.Fatalf("%+v.GroupIds(): %v", user, err) + } + if !containsID(gids, user.Gid) { + t.Errorf("%+v.GroupIds() = %v; does not contain user GID %s", user, gids, user.Gid) + } +} + +func containsID(ids []string, id string) bool { + for _, x := range ids { + if x == id { + return true + } + } + return false +} diff --git a/src/os/wait6_dragonfly.go b/src/os/wait6_dragonfly.go new file mode 100644 index 0000000..cc3af39 --- /dev/null +++ b/src/os/wait6_dragonfly.go @@ -0,0 +1,18 @@ +// Copyright 2022 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 os + +import ( + "syscall" + "unsafe" +) + +const _P_PID = 0 + +func wait6(idtype, id, options int) (status int, errno syscall.Errno) { + var status32 int32 // C.int + _, _, errno = syscall.Syscall6(syscall.SYS_WAIT6, uintptr(idtype), uintptr(id), uintptr(unsafe.Pointer(&status32)), uintptr(options), 0, 0) + return int(status32), errno +} diff --git a/src/os/wait6_freebsd64.go b/src/os/wait6_freebsd64.go new file mode 100644 index 0000000..b2677c5 --- /dev/null +++ b/src/os/wait6_freebsd64.go @@ -0,0 +1,20 @@ +// Copyright 2022 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. + +//go:build freebsd && (amd64 || arm64 || riscv64) + +package os + +import ( + "syscall" + "unsafe" +) + +const _P_PID = 0 + +func wait6(idtype, id, options int) (status int, errno syscall.Errno) { + var status32 int32 // C.int + _, _, errno = syscall.Syscall6(syscall.SYS_WAIT6, uintptr(idtype), uintptr(id), uintptr(unsafe.Pointer(&status32)), uintptr(options), 0, 0) + return int(status32), errno +} diff --git a/src/os/wait6_freebsd_386.go b/src/os/wait6_freebsd_386.go new file mode 100644 index 0000000..30b01c5 --- /dev/null +++ b/src/os/wait6_freebsd_386.go @@ -0,0 +1,18 @@ +// Copyright 2022 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 os + +import ( + "syscall" + "unsafe" +) + +const _P_PID = 0 + +func wait6(idtype, id, options int) (status int, errno syscall.Errno) { + // freebsd32_wait6_args{ idtype, id1, id2, status, options, wrusage, info } + _, _, errno = syscall.Syscall9(syscall.SYS_WAIT6, uintptr(idtype), uintptr(id), 0, uintptr(unsafe.Pointer(&status)), uintptr(options), 0, 0, 0, 0) + return status, errno +} diff --git a/src/os/wait6_freebsd_arm.go b/src/os/wait6_freebsd_arm.go new file mode 100644 index 0000000..0fd8af0 --- /dev/null +++ b/src/os/wait6_freebsd_arm.go @@ -0,0 +1,18 @@ +// Copyright 2022 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 os + +import ( + "syscall" + "unsafe" +) + +const _P_PID = 0 + +func wait6(idtype, id, options int) (status int, errno syscall.Errno) { + // freebsd32_wait6_args{ idtype, pad, id1, id2, status, options, wrusage, info } + _, _, errno = syscall.Syscall9(syscall.SYS_WAIT6, uintptr(idtype), 0, uintptr(id), 0, uintptr(unsafe.Pointer(&status)), uintptr(options), 0, 0, 0) + return status, errno +} diff --git a/src/os/wait6_netbsd.go b/src/os/wait6_netbsd.go new file mode 100644 index 0000000..0bbb73d --- /dev/null +++ b/src/os/wait6_netbsd.go @@ -0,0 +1,18 @@ +// Copyright 2022 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 os + +import ( + "syscall" + "unsafe" +) + +const _P_PID = 1 // not 0 as on FreeBSD and Dragonfly! + +func wait6(idtype, id, options int) (status int, errno syscall.Errno) { + var status32 int32 // C.int + _, _, errno = syscall.Syscall6(syscall.SYS_WAIT6, uintptr(idtype), uintptr(id), uintptr(unsafe.Pointer(&status32)), uintptr(options), 0, 0) + return int(status32), errno +} diff --git a/src/os/wait_unimp.go b/src/os/wait_unimp.go new file mode 100644 index 0000000..810e35d --- /dev/null +++ b/src/os/wait_unimp.go @@ -0,0 +1,21 @@ +// Copyright 2016 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. + +// aix, darwin, js/wasm, openbsd, solaris and wasip1/wasm don't implement +// waitid/wait6. + +//go:build aix || darwin || (js && wasm) || openbsd || solaris || wasip1 + +package os + +// blockUntilWaitable attempts to block until a call to p.Wait will +// succeed immediately, and reports whether it has done so. +// It does not actually call p.Wait. +// This version is used on systems that do not implement waitid, +// or where we have not implemented it yet. Note that this is racy: +// a call to Process.Signal can in an extremely unlikely case send a +// signal to the wrong process, see issue #13987. +func (p *Process) blockUntilWaitable() (bool, error) { + return false, nil +} diff --git a/src/os/wait_wait6.go b/src/os/wait_wait6.go new file mode 100644 index 0000000..1031428 --- /dev/null +++ b/src/os/wait_wait6.go @@ -0,0 +1,32 @@ +// Copyright 2016 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. + +//go:build dragonfly || freebsd || netbsd + +package os + +import ( + "runtime" + "syscall" +) + +// blockUntilWaitable attempts to block until a call to p.Wait will +// succeed immediately, and reports whether it has done so. +// It does not actually call p.Wait. +func (p *Process) blockUntilWaitable() (bool, error) { + var errno syscall.Errno + for { + _, errno = wait6(_P_PID, p.Pid, syscall.WEXITED|syscall.WNOWAIT) + if errno != syscall.EINTR { + break + } + } + runtime.KeepAlive(p) + if errno == syscall.ENOSYS { + return false, nil + } else if errno != 0 { + return false, NewSyscallError("wait6", errno) + } + return true, nil +} diff --git a/src/os/wait_waitid.go b/src/os/wait_waitid.go new file mode 100644 index 0000000..c0503b2 --- /dev/null +++ b/src/os/wait_waitid.go @@ -0,0 +1,48 @@ +// Copyright 2016 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. + +// We used to used this code for Darwin, but according to issue #19314 +// waitid returns if the process is stopped, even when using WEXITED. + +//go:build linux + +package os + +import ( + "runtime" + "syscall" + "unsafe" +) + +const _P_PID = 1 + +// blockUntilWaitable attempts to block until a call to p.Wait will +// succeed immediately, and reports whether it has done so. +// It does not actually call p.Wait. +func (p *Process) blockUntilWaitable() (bool, error) { + // The waitid system call expects a pointer to a siginfo_t, + // which is 128 bytes on all Linux systems. + // On darwin/amd64, it requires 104 bytes. + // We don't care about the values it returns. + var siginfo [16]uint64 + psig := &siginfo[0] + var e syscall.Errno + for { + _, _, e = syscall.Syscall6(syscall.SYS_WAITID, _P_PID, uintptr(p.Pid), uintptr(unsafe.Pointer(psig)), syscall.WEXITED|syscall.WNOWAIT, 0, 0) + if e != syscall.EINTR { + break + } + } + runtime.KeepAlive(p) + if e != 0 { + // waitid has been available since Linux 2.6.9, but + // reportedly is not available in Ubuntu on Windows. + // See issue 16610. + if e == syscall.ENOSYS { + return false, nil + } + return false, NewSyscallError("waitid", e) + } + return true, nil +} -- cgit v1.2.3