Commit a919b760 authored by Damien Neil's avatar Damien Neil

os: make errors.Is work with ErrPermission et al.

As proposed in Issue #29934, update errors produced by the os package to
work with errors.Is sentinel tests. For example,
errors.Is(err, os.ErrPermission) is equivalent to os.IsPermission(err)
with added unwrapping support.

Move the definition for os.ErrPermission and others into the syscall
package. Add an Is method to syscall.Errno and others. Add an Unwrap
method to os.PathError and others.

Updates #30322
Updates #29934

Change-Id: I95727d26c18a5354c720de316dff0bffc04dd926
Reviewed-on: https://go-review.googlesource.com/c/go/+/163058Reviewed-by: default avatarMarcel van Lohuizen <mpvl@golang.org>
parent af7b7571
...@@ -157,8 +157,8 @@ func TestErrorFormatter(t *testing.T) { ...@@ -157,8 +157,8 @@ func TestErrorFormatter(t *testing.T) {
want: "fallback:" + want: "fallback:" +
"\n somefile.go:123" + "\n somefile.go:123" +
"\n - file does not exist:" + "\n - file does not exist:" +
"\n os.init" + "\n .*" +
"\n .+/os/error.go:\\d\\d", "\n .+.go:\\d+",
regexp: true, regexp: true,
}, { }, {
err: &wrapped{"outer", err: &wrapped{"outer",
......
...@@ -146,8 +146,9 @@ var pkgDeps = map[string][]string{ ...@@ -146,8 +146,9 @@ var pkgDeps = map[string][]string{
// End of linear dependency definitions. // End of linear dependency definitions.
// Operating system access. // Operating system access.
"syscall": {"L0", "internal/race", "internal/syscall/windows/sysdll", "syscall/js", "unicode/utf16"}, "syscall": {"L0", "internal/oserror", "internal/race", "internal/syscall/windows/sysdll", "syscall/js", "unicode/utf16"},
"syscall/js": {"L0"}, "syscall/js": {"L0"},
"internal/oserror": {"L0"},
"internal/syscall/unix": {"L0", "syscall"}, "internal/syscall/unix": {"L0", "syscall"},
"internal/syscall/windows": {"L0", "syscall", "internal/syscall/windows/sysdll"}, "internal/syscall/windows": {"L0", "syscall", "internal/syscall/windows/sysdll"},
"internal/syscall/windows/registry": {"L0", "syscall", "internal/syscall/windows/sysdll", "unicode/utf16"}, "internal/syscall/windows/registry": {"L0", "syscall", "internal/syscall/windows/sysdll", "unicode/utf16"},
...@@ -167,7 +168,7 @@ var pkgDeps = map[string][]string{ ...@@ -167,7 +168,7 @@ var pkgDeps = map[string][]string{
"internal/poll": {"L0", "internal/race", "syscall", "time", "unicode/utf16", "unicode/utf8", "internal/syscall/windows"}, "internal/poll": {"L0", "internal/race", "syscall", "time", "unicode/utf16", "unicode/utf8", "internal/syscall/windows"},
"internal/testlog": {"L0"}, "internal/testlog": {"L0"},
"os": {"L1", "os", "syscall", "time", "internal/poll", "internal/syscall/windows", "internal/syscall/unix", "internal/testlog"}, "os": {"L1", "os", "syscall", "time", "internal/oserror", "internal/poll", "internal/syscall/windows", "internal/syscall/unix", "internal/testlog"},
"path/filepath": {"L2", "os", "syscall", "internal/syscall/windows"}, "path/filepath": {"L2", "os", "syscall", "internal/syscall/windows"},
"io/ioutil": {"L2", "os", "path/filepath", "time"}, "io/ioutil": {"L2", "os", "path/filepath", "time"},
"os/exec": {"L2", "os", "context", "path/filepath", "syscall"}, "os/exec": {"L2", "os", "context", "path/filepath", "syscall"},
......
// 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 oserror defines errors values used in the os package.
//
// These types are defined here to permit the syscall package to reference them.
package oserror
import "errors"
var (
ErrInvalid = errors.New("invalid argument")
ErrPermission = errors.New("permission denied")
ErrExist = errors.New("file already exists")
ErrNotExist = errors.New("file does not exist")
ErrClosed = errors.New("file already closed")
ErrTemporary = errors.New("temporary error")
ErrTimeout = errors.New("deadline exceeded")
)
...@@ -5,20 +5,35 @@ ...@@ -5,20 +5,35 @@
package os package os
import ( import (
"errors" "internal/oserror"
"internal/poll" "internal/poll"
) )
// Portable analogs of some common system call errors. // Portable analogs of some common system call errors.
//
// Errors returned from this package may be tested against these errors
// with errors.Is.
var ( var (
ErrInvalid = errors.New("invalid argument") // methods on File will return this error when the receiver is nil // ErrInvalid indicates an invalid argument.
ErrPermission = errors.New("permission denied") // Methods on File will return this error when the receiver is nil.
ErrExist = errors.New("file already exists") ErrInvalid = errInvalid() // "invalid argument"
ErrNotExist = errors.New("file does not exist")
ErrClosed = errors.New("file already closed") ErrPermission = errPermission() // "permission denied"
ErrNoDeadline = poll.ErrNoDeadline ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
ErrTimeout = errTimeout() // "deadline exceeded"
ErrNoDeadline = errNoDeadline() // "file type does not support deadline"
) )
func errInvalid() error { return oserror.ErrInvalid }
func errPermission() error { return oserror.ErrPermission }
func errExist() error { return oserror.ErrExist }
func errNotExist() error { return oserror.ErrNotExist }
func errClosed() error { return oserror.ErrClosed }
func errTimeout() error { return oserror.ErrTimeout }
func errNoDeadline() error { return poll.ErrNoDeadline }
type timeout interface { type timeout interface {
Timeout() bool Timeout() bool
} }
...@@ -48,6 +63,8 @@ type SyscallError struct { ...@@ -48,6 +63,8 @@ type SyscallError struct {
func (e *SyscallError) Error() string { return e.Syscall + ": " + e.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. // Timeout reports whether this error represents a timeout.
func (e *SyscallError) Timeout() bool { func (e *SyscallError) Timeout() bool {
t, ok := e.Err.(timeout) t, ok := e.Err.(timeout)
...@@ -68,21 +85,21 @@ func NewSyscallError(syscall string, err error) error { ...@@ -68,21 +85,21 @@ func NewSyscallError(syscall string, err error) error {
// that a file or directory already exists. It is satisfied by ErrExist as // that a file or directory already exists. It is satisfied by ErrExist as
// well as some syscall errors. // well as some syscall errors.
func IsExist(err error) bool { func IsExist(err error) bool {
return isExist(err) return underlyingErrorIs(err, ErrExist)
} }
// IsNotExist returns a boolean indicating whether the error is known to // IsNotExist returns a boolean indicating whether the error is known to
// report that a file or directory does not exist. It is satisfied by // report that a file or directory does not exist. It is satisfied by
// ErrNotExist as well as some syscall errors. // ErrNotExist as well as some syscall errors.
func IsNotExist(err error) bool { func IsNotExist(err error) bool {
return isNotExist(err) return underlyingErrorIs(err, ErrNotExist)
} }
// IsPermission returns a boolean indicating whether the error is known to // IsPermission returns a boolean indicating whether the error is known to
// report that permission is denied. It is satisfied by ErrPermission as well // report that permission is denied. It is satisfied by ErrPermission as well
// as some syscall errors. // as some syscall errors.
func IsPermission(err error) bool { func IsPermission(err error) bool {
return isPermission(err) return underlyingErrorIs(err, ErrPermission)
} }
// IsTimeout returns a boolean indicating whether the error is known // IsTimeout returns a boolean indicating whether the error is known
...@@ -92,6 +109,18 @@ func IsTimeout(err error) bool { ...@@ -92,6 +109,18 @@ func IsTimeout(err error) bool {
return ok && terr.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.Wrapper implementations.
err = underlyingError(err)
if err == target {
return true
}
e, ok := err.(interface{ Is(error) bool })
return ok && e.Is(target)
}
// underlyingError returns the underlying error for known os error types. // underlyingError returns the underlying error for known os error types.
func underlyingError(err error) error { func underlyingError(err error) error {
switch err := err.(type) { switch err := err.(type) {
......
// 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 isExist(err error) bool {
return checkErrMessageContent(err, "exists", "is a directory")
}
func isNotExist(err error) bool {
return checkErrMessageContent(err, "does not exist", "not found",
"has been removed", "no parent")
}
func isPermission(err error) bool {
return checkErrMessageContent(err, "permission denied")
}
// checkErrMessageContent checks if err message contains one of msgs.
func checkErrMessageContent(err error, msgs ...string) bool {
if err == nil {
return false
}
err = underlyingError(err)
for _, msg := range msgs {
if contains(err.Error(), msg) {
return true
}
}
return false
}
// contains is a local version of strings.Contains. It knows len(sep) > 1.
func contains(s, sep string) bool {
n := len(sep)
c := sep[0]
for i := 0; i+n <= len(s); i++ {
if s[i] == c && s[i:i+n] == sep {
return true
}
}
return false
}
...@@ -27,7 +27,7 @@ func TestErrIsExist(t *testing.T) { ...@@ -27,7 +27,7 @@ func TestErrIsExist(t *testing.T) {
t.Fatal("Open should have failed") t.Fatal("Open should have failed")
return return
} }
if s := checkErrorPredicate("os.IsExist", os.IsExist, err); s != "" { if s := checkErrorPredicate("os.IsExist", os.IsExist, err, os.ErrExist); s != "" {
t.Fatal(s) t.Fatal(s)
return return
} }
...@@ -39,7 +39,7 @@ func testErrNotExist(name string) string { ...@@ -39,7 +39,7 @@ func testErrNotExist(name string) string {
f.Close() f.Close()
return "Open should have failed" return "Open should have failed"
} }
if s := checkErrorPredicate("os.IsNotExist", os.IsNotExist, err); s != "" { if s := checkErrorPredicate("os.IsNotExist", os.IsNotExist, err, os.ErrNotExist); s != "" {
return s return s
} }
...@@ -47,7 +47,7 @@ func testErrNotExist(name string) string { ...@@ -47,7 +47,7 @@ func testErrNotExist(name string) string {
if err == nil { if err == nil {
return "Chdir should have failed" return "Chdir should have failed"
} }
if s := checkErrorPredicate("os.IsNotExist", os.IsNotExist, err); s != "" { if s := checkErrorPredicate("os.IsNotExist", os.IsNotExist, err, os.ErrNotExist); s != "" {
return s return s
} }
return "" return ""
...@@ -74,10 +74,13 @@ func TestErrIsNotExist(t *testing.T) { ...@@ -74,10 +74,13 @@ func TestErrIsNotExist(t *testing.T) {
} }
} }
func checkErrorPredicate(predName string, pred func(error) bool, err error) string { func checkErrorPredicate(predName string, pred func(error) bool, err, target error) string {
if !pred(err) { if !pred(err) {
return fmt.Sprintf("%s does not work as expected for %#v", predName, 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 "" return ""
} }
...@@ -108,9 +111,15 @@ func TestIsExist(t *testing.T) { ...@@ -108,9 +111,15 @@ func TestIsExist(t *testing.T) {
if is := os.IsExist(tt.err); is != tt.is { 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) t.Errorf("os.IsExist(%T %v) = %v, want %v", tt.err, tt.err, is, tt.is)
} }
if is := errors.Is(tt.err, os.ErrExist); is != tt.is {
t.Errorf("errors.Is(%T %v, os.ErrExist) = %v, want %v", tt.err, tt.err, is, tt.is)
}
if isnot := os.IsNotExist(tt.err); isnot != tt.isnot { 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) t.Errorf("os.IsNotExist(%T %v) = %v, want %v", tt.err, tt.err, isnot, tt.isnot)
} }
if isnot := errors.Is(tt.err, os.ErrNotExist); isnot != tt.isnot {
t.Errorf("errors.Is(%T %v, os.ErrNotExist) = %v, want %v", tt.err, tt.err, isnot, tt.isnot)
}
} }
} }
...@@ -130,6 +139,9 @@ func TestIsPermission(t *testing.T) { ...@@ -130,6 +139,9 @@ func TestIsPermission(t *testing.T) {
if got := os.IsPermission(tt.err); got != tt.want { if got := os.IsPermission(tt.err); got != tt.want {
t.Errorf("os.IsPermission(%#v) = %v; want %v", tt.err, got, tt.want) t.Errorf("os.IsPermission(%#v) = %v; want %v", tt.err, got, tt.want)
} }
if got := errors.Is(tt.err, os.ErrPermission); got != tt.want {
t.Errorf("errors.Is(%#v, os.ErrPermission) = %v; want %v", tt.err, got, tt.want)
}
} }
} }
......
// 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.
// +build aix darwin dragonfly freebsd js,wasm linux nacl netbsd openbsd solaris
package os
import "syscall"
func isExist(err error) bool {
err = underlyingError(err)
return err == syscall.EEXIST || err == syscall.ENOTEMPTY || err == ErrExist
}
func isNotExist(err error) bool {
err = underlyingError(err)
return err == syscall.ENOENT || err == ErrNotExist
}
func isPermission(err error) bool {
err = underlyingError(err)
return err == syscall.EACCES || err == syscall.EPERM || err == ErrPermission
}
// 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
import "syscall"
func isExist(err error) bool {
err = underlyingError(err)
return err == syscall.ERROR_ALREADY_EXISTS ||
err == syscall.ERROR_DIR_NOT_EMPTY ||
err == syscall.ERROR_FILE_EXISTS || err == ErrExist
}
const _ERROR_BAD_NETPATH = syscall.Errno(53)
func isNotExist(err error) bool {
err = underlyingError(err)
return err == syscall.ERROR_FILE_NOT_FOUND ||
err == _ERROR_BAD_NETPATH ||
err == syscall.ERROR_PATH_NOT_FOUND || err == ErrNotExist
}
func isPermission(err error) bool {
err = underlyingError(err)
return err == syscall.ERROR_ACCESS_DENIED || err == ErrPermission
}
...@@ -98,6 +98,10 @@ func (e *LinkError) Error() string { ...@@ -98,6 +98,10 @@ func (e *LinkError) Error() string {
return e.Op + " " + e.Old + " " + e.New + ": " + e.Err.Error() 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. // Read reads up to len(b) bytes from the File.
// It returns the number of bytes read and any error encountered. // It returns the number of bytes read and any error encountered.
// At end of file, Read returns 0, io.EOF. // At end of file, Read returns 0, io.EOF.
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
package syscall package syscall
import ( import (
"internal/oserror"
"sync" "sync"
"unsafe" "unsafe"
) )
...@@ -55,6 +56,22 @@ func (e Errno) Error() string { ...@@ -55,6 +56,22 @@ func (e Errno) Error() string {
return "errno " + itoa(int(e)) return "errno " + itoa(int(e))
} }
func (e Errno) Is(target error) bool {
switch target {
case oserror.ErrTemporary:
return e.Temporary()
case oserror.ErrTimeout:
return e.Timeout()
case oserror.ErrPermission:
return e == EACCES || e == EPERM
case oserror.ErrExist:
return e == EEXIST || e == ENOTEMPTY
case oserror.ErrNotExist:
return e == ENOENT
}
return false
}
func (e Errno) Temporary() bool { func (e Errno) Temporary() bool {
return e == EINTR || e == EMFILE || e.Timeout() return e == EINTR || e == EMFILE || e.Timeout()
} }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package syscall package syscall
import ( import (
"internal/oserror"
"sync" "sync"
"unsafe" "unsafe"
) )
...@@ -62,6 +63,22 @@ func (e Errno) Error() string { ...@@ -62,6 +63,22 @@ func (e Errno) Error() string {
return "errno " + itoa(int(e)) return "errno " + itoa(int(e))
} }
func (e Errno) Is(target error) bool {
switch target {
case oserror.ErrTemporary:
return e.Temporary()
case oserror.ErrTimeout:
return e.Timeout()
case oserror.ErrPermission:
return e == EACCES || e == EPERM
case oserror.ErrExist:
return e == EEXIST || e == ENOTEMPTY
case oserror.ErrNotExist:
return e == ENOENT
}
return false
}
func (e Errno) Temporary() bool { func (e Errno) Temporary() bool {
return e == EINTR || e == EMFILE || e.Timeout() return e == EINTR || e == EMFILE || e.Timeout()
} }
......
...@@ -11,7 +11,10 @@ ...@@ -11,7 +11,10 @@
package syscall package syscall
import "unsafe" import (
"internal/oserror"
"unsafe"
)
const ImplementsGetwd = true const ImplementsGetwd = true
const bitSize16 = 2 const bitSize16 = 2
...@@ -24,6 +27,45 @@ func (e ErrorString) Error() string { return string(e) } ...@@ -24,6 +27,45 @@ func (e ErrorString) Error() string { return string(e) }
// NewError converts s to an ErrorString, which satisfies the Error interface. // NewError converts s to an ErrorString, which satisfies the Error interface.
func NewError(s string) error { return ErrorString(s) } func NewError(s string) error { return ErrorString(s) }
func (e ErrorString) Is(target error) bool {
switch target {
case oserror.ErrTemporary:
return e.Temporary()
case oserror.ErrTimeout:
return e.Timeout()
case oserror.ErrPermission:
return checkErrMessageContent(e, "permission denied")
case oserror.ErrExist:
return checkErrMessageContent(e, "exists", "is a directory")
case oserror.ErrNotExist:
return checkErrMessageContent(e, "does not exist", "not found",
"has been removed", "no parent")
}
return false
}
// checkErrMessageContent checks if err message contains one of msgs.
func checkErrMessageContent(e ErrorString, msgs ...string) bool {
for _, msg := range msgs {
if contains(string(e), msg) {
return true
}
}
return false
}
// contains is a local version of strings.Contains. It knows len(sep) > 1.
func contains(s, sep string) bool {
n := len(sep)
c := sep[0]
for i := 0; i+n <= len(s); i++ {
if s[i] == c && s[i:i+n] == sep {
return true
}
}
return false
}
func (e ErrorString) Temporary() bool { func (e ErrorString) Temporary() bool {
return e == EINTR || e == EMFILE || e.Timeout() return e == EINTR || e == EMFILE || e.Timeout()
} }
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
package syscall package syscall
import ( import (
"internal/oserror"
"internal/race" "internal/race"
"runtime" "runtime"
"sync" "sync"
...@@ -120,6 +121,22 @@ func (e Errno) Error() string { ...@@ -120,6 +121,22 @@ func (e Errno) Error() string {
return "errno " + itoa(int(e)) return "errno " + itoa(int(e))
} }
func (e Errno) Is(target error) bool {
switch target {
case oserror.ErrTemporary:
return e.Temporary()
case oserror.ErrTimeout:
return e.Timeout()
case oserror.ErrPermission:
return e == EACCES || e == EPERM
case oserror.ErrExist:
return e == EEXIST || e == ENOTEMPTY
case oserror.ErrNotExist:
return e == ENOENT
}
return false
}
func (e Errno) Temporary() bool { func (e Errno) Temporary() bool {
return e == EINTR || e == EMFILE || e.Timeout() return e == EINTR || e == EMFILE || e.Timeout()
} }
......
...@@ -8,6 +8,7 @@ package syscall ...@@ -8,6 +8,7 @@ package syscall
import ( import (
errorspkg "errors" errorspkg "errors"
"internal/oserror"
"internal/race" "internal/race"
"runtime" "runtime"
"sync" "sync"
...@@ -110,6 +111,28 @@ func (e Errno) Error() string { ...@@ -110,6 +111,28 @@ func (e Errno) Error() string {
return string(utf16.Decode(b[:n])) return string(utf16.Decode(b[:n]))
} }
const _ERROR_BAD_NETPATH = Errno(53)
func (e Errno) Is(target error) bool {
switch target {
case oserror.ErrTemporary:
return e.Temporary()
case oserror.ErrTimeout:
return e.Timeout()
case oserror.ErrPermission:
return e == ERROR_ACCESS_DENIED
case oserror.ErrExist:
return e == ERROR_ALREADY_EXISTS ||
e == ERROR_DIR_NOT_EMPTY ||
e == ERROR_FILE_EXISTS
case oserror.ErrNotExist:
return e == ERROR_FILE_NOT_FOUND ||
e == _ERROR_BAD_NETPATH ||
e == ERROR_PATH_NOT_FOUND
}
return false
}
func (e Errno) Temporary() bool { func (e Errno) Temporary() bool {
return e == EINTR || e == EMFILE || e.Timeout() return e == EINTR || e == EMFILE || e.Timeout()
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment