| // Copyright 2024 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 |
| |
| import ( |
| "errors" |
| "internal/filepathlite" |
| "internal/stringslite" |
| "internal/syscall/windows" |
| "runtime" |
| "syscall" |
| "time" |
| "unsafe" |
| ) |
| |
| // rootCleanPath uses GetFullPathName to perform lexical path cleaning. |
| // |
| // On Windows, file names are lexically cleaned at the start of a file operation. |
| // For example, on Windows the path `a\..\b` is exactly equivalent to `b` alone, |
| // even if `a` does not exist or is not a directory. |
| // |
| // We use the Windows API function GetFullPathName to perform this cleaning. |
| // We could do this ourselves, but there are a number of subtle behaviors here, |
| // and deferring to the OS maintains consistency. |
| // (For example, `a\.\` cleans to `a\`.) |
| // |
| // GetFullPathName operates on absolute paths, and our input path is relative. |
| // We make the path absolute by prepending a fixed prefix of \\?\?\. |
| // |
| // We want to detect paths which use .. components to escape the root. |
| // We do this by ensuring the cleaned path still begins with \\?\?\. |
| // We catch the corner case of a path which includes a ..\?\. component |
| // by rejecting any input paths which contain a ?, which is not a valid character |
| // in a Windows filename. |
| func rootCleanPath(s string, prefix, suffix []string) (string, error) { |
| // Reject paths which include a ? component (see above). |
| if stringslite.IndexByte(s, '?') >= 0 { |
| return "", windows.ERROR_INVALID_NAME |
| } |
| |
| const fixedPrefix = `\\?\?` |
| buf := []byte(fixedPrefix) |
| for _, p := range prefix { |
| buf = append(buf, '\\') |
| buf = append(buf, []byte(p)...) |
| } |
| buf = append(buf, '\\') |
| buf = append(buf, []byte(s)...) |
| for _, p := range suffix { |
| buf = append(buf, '\\') |
| buf = append(buf, []byte(p)...) |
| } |
| s = string(buf) |
| |
| s, err := syscall.FullPath(s) |
| if err != nil { |
| return "", err |
| } |
| |
| s, ok := stringslite.CutPrefix(s, fixedPrefix) |
| if !ok { |
| return "", errPathEscapes |
| } |
| s = stringslite.TrimPrefix(s, `\`) |
| if s == "" { |
| s = "." |
| } |
| |
| if !filepathlite.IsLocal(s) { |
| return "", errPathEscapes |
| } |
| |
| return s, nil |
| } |
| |
| // sysfdType is the native type of a file handle |
| // (int on Unix, syscall.Handle on Windows), |
| // permitting helper functions to be written portably. |
| type sysfdType = syscall.Handle |
| |
| // openRootNolog is OpenRoot. |
| func openRootNolog(name string) (*Root, error) { |
| if name == "" { |
| return nil, &PathError{Op: "open", Path: name, Err: syscall.ENOENT} |
| } |
| path := fixLongPath(name) |
| fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_CLOEXEC, 0) |
| if err != nil { |
| return nil, &PathError{Op: "open", Path: name, Err: err} |
| } |
| return newRoot(fd, name) |
| } |
| |
| // newRoot returns a new Root. |
| // If fd is not a directory, it closes it and returns an error. |
| func newRoot(fd syscall.Handle, name string) (*Root, error) { |
| // Check that this is a directory. |
| // |
| // If we get any errors here, ignore them; worst case we create a Root |
| // which returns errors when you try to use it. |
| var fi syscall.ByHandleFileInformation |
| err := syscall.GetFileInformationByHandle(fd, &fi) |
| if err == nil && fi.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 { |
| syscall.CloseHandle(fd) |
| return nil, &PathError{Op: "open", Path: name, Err: errors.New("not a directory")} |
| } |
| |
| r := &Root{&root{ |
| fd: fd, |
| name: name, |
| }} |
| runtime.SetFinalizer(r.root, (*root).Close) |
| return r, nil |
| } |
| |
| // openRootInRoot is Root.OpenRoot. |
| func openRootInRoot(r *Root, name string) (*Root, error) { |
| fd, err := doInRoot(r, name, nil, rootOpenDir) |
| if err != nil { |
| return nil, &PathError{Op: "openat", Path: name, Err: err} |
| } |
| return newRoot(fd, name) |
| } |
| |
| // rootOpenFileNolog is Root.OpenFile. |
| func rootOpenFileNolog(root *Root, name string, flag int, perm FileMode) (*File, error) { |
| fd, err := doInRoot(root, name, nil, func(parent syscall.Handle, name string) (syscall.Handle, error) { |
| return openat(parent, name, uint64(flag), perm) |
| }) |
| if err != nil { |
| return nil, &PathError{Op: "openat", Path: name, Err: err} |
| } |
| // openat always returns a non-blocking handle. |
| return newFile(fd, joinPath(root.Name(), name), "file", false), nil |
| } |
| |
| func openat(dirfd syscall.Handle, name string, flag uint64, perm FileMode) (syscall.Handle, error) { |
| h, err := windows.Openat(dirfd, name, flag|syscall.O_CLOEXEC|windows.O_NOFOLLOW_ANY, syscallMode(perm)) |
| if err == syscall.ELOOP || err == syscall.ENOTDIR { |
| if link, err := readReparseLinkAt(dirfd, name); err == nil { |
| return syscall.InvalidHandle, errSymlink(link) |
| } |
| } |
| return h, err |
| } |
| |
| func readReparseLinkAt(dirfd syscall.Handle, name string) (string, error) { |
| objectName, err := windows.NewNTUnicodeString(name) |
| if err != nil { |
| return "", err |
| } |
| objAttrs := &windows.OBJECT_ATTRIBUTES{ |
| ObjectName: objectName, |
| } |
| if dirfd != syscall.InvalidHandle { |
| objAttrs.RootDirectory = dirfd |
| } |
| objAttrs.Length = uint32(unsafe.Sizeof(*objAttrs)) |
| var h syscall.Handle |
| err = windows.NtCreateFile( |
| &h, |
| windows.FILE_GENERIC_READ, |
| objAttrs, |
| &windows.IO_STATUS_BLOCK{}, |
| nil, |
| uint32(syscall.FILE_ATTRIBUTE_NORMAL), |
| syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, |
| windows.FILE_OPEN, |
| windows.FILE_SYNCHRONOUS_IO_NONALERT|windows.FILE_OPEN_REPARSE_POINT, |
| nil, |
| 0, |
| ) |
| if err != nil { |
| return "", err |
| } |
| defer syscall.CloseHandle(h) |
| return readReparseLinkHandle(h) |
| } |
| |
| func rootOpenDir(parent syscall.Handle, name string) (syscall.Handle, error) { |
| h, err := openat(parent, name, syscall.O_RDONLY|syscall.O_CLOEXEC|windows.O_DIRECTORY, 0) |
| if err == syscall.ERROR_FILE_NOT_FOUND { |
| // Windows returns: |
| // - ERROR_PATH_NOT_FOUND if any path compoenent before the leaf |
| // does not exist or is not a directory. |
| // - ERROR_FILE_NOT_FOUND if the leaf does not exist. |
| // |
| // This differs from Unix behavior, which is: |
| // - ENOENT if any path component does not exist, including the leaf. |
| // - ENOTDIR if any path component before the leaf is not a directory. |
| // |
| // We map syscall.ENOENT to ERROR_FILE_NOT_FOUND and syscall.ENOTDIR |
| // to ERROR_PATH_NOT_FOUND, but the Windows errors don't quite match. |
| // |
| // For consistency with os.Open, convert ERROR_FILE_NOT_FOUND here into |
| // ERROR_PATH_NOT_FOUND, since we're opening a non-leaf path component. |
| err = syscall.ERROR_PATH_NOT_FOUND |
| } |
| return h, err |
| } |
| |
| func rootStat(r *Root, name string, lstat bool) (FileInfo, error) { |
| if len(name) > 0 && IsPathSeparator(name[len(name)-1]) { |
| // When a filename ends with a path separator, |
| // Lstat behaves like Stat. |
| // |
| // This behavior is not based on a principled decision here, |
| // merely the empirical evidence that Lstat behaves this way. |
| lstat = false |
| } |
| fi, err := doInRoot(r, name, nil, func(parent syscall.Handle, n string) (FileInfo, error) { |
| fd, err := openat(parent, n, windows.O_OPEN_REPARSE, 0) |
| if err != nil { |
| return nil, err |
| } |
| defer syscall.CloseHandle(fd) |
| fi, err := statHandle(name, fd) |
| if err != nil { |
| return nil, err |
| } |
| if !lstat && fi.(*fileStat).isReparseTagNameSurrogate() { |
| link, err := readReparseLinkHandle(fd) |
| if err != nil { |
| return nil, err |
| } |
| return nil, errSymlink(link) |
| } |
| return fi, nil |
| }) |
| if err != nil { |
| return nil, &PathError{Op: "statat", Path: name, Err: err} |
| } |
| return fi, nil |
| } |
| |
| func rootSymlink(r *Root, oldname, newname string) error { |
| if oldname == "" { |
| return syscall.EINVAL |
| } |
| |
| // CreateSymbolicLinkW converts volume-relative paths into absolute ones. |
| // Do the same. |
| if filepathlite.VolumeNameLen(oldname) > 0 && !filepathlite.IsAbs(oldname) { |
| p, err := syscall.FullPath(oldname) |
| if err == nil { |
| oldname = p |
| } |
| } |
| |
| // If oldname can be resolved to a directory in the root, create a directory link. |
| // Otherwise, create a file link. |
| var flags windows.SymlinkatFlags |
| if filepathlite.VolumeNameLen(oldname) == 0 && !IsPathSeparator(oldname[0]) { |
| // oldname is a path relative to the directory containing newname. |
| // Prepend newname's directory to it to make a path relative to the root. |
| // For example, if oldname=old and newname=a\new, destPath=a\old. |
| destPath := oldname |
| if dir := dirname(newname); dir != "." { |
| destPath = dir + `\` + oldname |
| } |
| fi, err := r.Stat(destPath) |
| if err == nil && fi.IsDir() { |
| flags |= windows.SYMLINKAT_DIRECTORY |
| } |
| } |
| |
| // Empirically, CreateSymbolicLinkW appears to set the relative flag iff |
| // the target does not contain a volume name. |
| if filepathlite.VolumeNameLen(oldname) == 0 { |
| flags |= windows.SYMLINKAT_RELATIVE |
| } |
| |
| _, err := doInRoot(r, newname, nil, func(parent sysfdType, name string) (struct{}, error) { |
| return struct{}{}, windows.Symlinkat(oldname, parent, name, flags) |
| }) |
| if err != nil { |
| return &LinkError{"symlinkat", oldname, newname, err} |
| } |
| return nil |
| } |
| |
| func chmodat(parent syscall.Handle, name string, mode FileMode) error { |
| // Currently, on Windows os.Chmod("symlink") will act on "symlink", |
| // not on any file it points to. |
| // |
| // This may or may not be the desired behavior: https://go.dev/issue/71492 |
| // |
| // For now, be consistent with os.Symlink. |
| // Passing O_OPEN_REPARSE causes us to open the named file itself, |
| // not any file that it links to. |
| // |
| // If we want to change this in the future, pass O_NOFOLLOW_ANY instead |
| // and return errSymlink when encountering a symlink: |
| // |
| // if err == syscall.ELOOP || err == syscall.ENOTDIR { |
| // if link, err := readReparseLinkAt(parent, name); err == nil { |
| // return errSymlink(link) |
| // } |
| // } |
| h, err := windows.Openat(parent, name, syscall.O_CLOEXEC|windows.O_OPEN_REPARSE|windows.O_WRITE_ATTRS, 0) |
| if err != nil { |
| return err |
| } |
| defer syscall.CloseHandle(h) |
| |
| var d syscall.ByHandleFileInformation |
| if err := syscall.GetFileInformationByHandle(h, &d); err != nil { |
| return err |
| } |
| attrs := d.FileAttributes |
| |
| if mode&syscall.S_IWRITE != 0 { |
| attrs &^= syscall.FILE_ATTRIBUTE_READONLY |
| } else { |
| attrs |= syscall.FILE_ATTRIBUTE_READONLY |
| } |
| if attrs == d.FileAttributes { |
| return nil |
| } |
| |
| var fbi windows.FILE_BASIC_INFO |
| fbi.FileAttributes = attrs |
| return windows.SetFileInformationByHandle(h, windows.FileBasicInfo, unsafe.Pointer(&fbi), uint32(unsafe.Sizeof(fbi))) |
| } |
| |
| func chownat(parent syscall.Handle, name string, uid, gid int) error { |
| return syscall.EWINDOWS // matches syscall.Chown |
| } |
| |
| func lchownat(parent syscall.Handle, name string, uid, gid int) error { |
| return syscall.EWINDOWS // matches syscall.Lchown |
| } |
| |
| func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error { |
| return windows.Mkdirat(dirfd, name, syscallMode(perm)) |
| } |
| |
| func removeat(dirfd syscall.Handle, name string) error { |
| return windows.Deleteat(dirfd, name, 0) |
| } |
| |
| func removefileat(dirfd syscall.Handle, name string) error { |
| return windows.Deleteat(dirfd, name, windows.FILE_NON_DIRECTORY_FILE) |
| } |
| |
| func removedirat(dirfd syscall.Handle, name string) error { |
| return windows.Deleteat(dirfd, name, windows.FILE_DIRECTORY_FILE) |
| } |
| |
| func chtimesat(dirfd syscall.Handle, name string, atime time.Time, mtime time.Time) error { |
| h, err := windows.Openat(dirfd, name, syscall.O_CLOEXEC|windows.O_NOFOLLOW_ANY|windows.O_WRITE_ATTRS, 0) |
| if err == syscall.ELOOP || err == syscall.ENOTDIR { |
| if link, err := readReparseLinkAt(dirfd, name); err == nil { |
| return errSymlink(link) |
| } |
| } |
| if err != nil { |
| return err |
| } |
| defer syscall.CloseHandle(h) |
| a := syscall.Filetime{} |
| w := syscall.Filetime{} |
| if !atime.IsZero() { |
| a = syscall.NsecToFiletime(atime.UnixNano()) |
| } |
| if !mtime.IsZero() { |
| w = syscall.NsecToFiletime(mtime.UnixNano()) |
| } |
| return syscall.SetFileTime(h, nil, &a, &w) |
| } |
| |
| func renameat(oldfd syscall.Handle, oldname string, newfd syscall.Handle, newname string) error { |
| return windows.Renameat(oldfd, oldname, newfd, newname) |
| } |
| |
| func linkat(oldfd syscall.Handle, oldname string, newfd syscall.Handle, newname string) error { |
| return windows.Linkat(oldfd, oldname, newfd, newname) |
| } |
| |
| func readlinkat(dirfd syscall.Handle, name string) (string, error) { |
| fd, err := openat(dirfd, name, windows.O_OPEN_REPARSE, 0) |
| if err != nil { |
| return "", err |
| } |
| defer syscall.CloseHandle(fd) |
| return readReparseLinkHandle(fd) |
| } |
| |
| func modeAt(parent syscall.Handle, name string) (FileMode, error) { |
| fd, err := openat(parent, name, windows.O_OPEN_REPARSE|windows.O_DIRECTORY, 0) |
| if err != nil { |
| return 0, err |
| } |
| defer syscall.CloseHandle(fd) |
| fi, err := statHandle(name, fd) |
| if err != nil { |
| return 0, err |
| } |
| return fi.Mode(), nil |
| } |