Skip to content

Commit 102bafb

Browse files
authored
Merge pull request #54 from knz/20201028-oserror
New sub-package `oserror`.
2 parents 59b590b + e4ae168 commit 102bafb

20 files changed

+1989
-1300
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Table of contents:
4747
| wrappers to attach [`logtags`](https://github.com/cockroachdb/logtags) details from `context.Context` | | | ||
4848
| `errors.FormatError()`, `Formatter`, `Printer` | | | (under construction) ||
4949
| `errors.SafeFormatError()`, `SafeFormatter` | | | ||
50+
| wrapper-aware `IsPermission()`, `IsTimeout()`, `IsExist()`, `IsNotExist()` | | | ||
5051

5152
"Forward compatibility" above refers to the ability of this library to
5253
recognize and properly handle network communication of error types it
@@ -61,6 +62,8 @@ older version of the package.
6162
- test error identity with `errors.Is()` as usual.
6263
**Unique in this library**: this works even if the error has traversed the network!
6364
Also, `errors.IsAny()` to recognize two or more reference errors.
65+
- replace uses of `os.IsPermission()`, `os.IsTimeout()`, `os.IsExist()` and `os.IsNotExist()` by their analog in sub-package `oserror` so
66+
that they can peek through layers of wrapping.
6467
- access error causes with `errors.UnwrapOnce()` / `errors.UnwrapAll()` (note: `errors.Cause()` and `errors.Unwrap()` also provided for compatibility with other error packages).
6568
- encode/decode errors to protobuf with `errors.EncodeError()` / `errors.DecodeError()`.
6669
- extract **PII-free safe details** with `errors.GetSafeDetails()`.

errbase/adapters.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import (
1818
"context"
1919
goErr "errors"
2020
"fmt"
21+
"os"
2122

23+
"github.com/cockroachdb/errors/errorspb"
2224
"github.com/gogo/protobuf/proto"
2325
pkgErr "github.com/pkg/errors"
2426
)
@@ -75,6 +77,108 @@ func encodePkgWithStack(
7577
return "" /* withStack does not have a message prefix */, safeDetails, nil
7678
}
7779

80+
func encodePathError(
81+
_ context.Context, err error,
82+
) (msgPrefix string, safe []string, details proto.Message) {
83+
p := err.(*os.PathError)
84+
msg := p.Op + " " + p.Path
85+
details = &errorspb.StringsPayload{
86+
Details: []string{p.Op, p.Path},
87+
}
88+
return msg, []string{p.Op}, details
89+
}
90+
91+
func decodePathError(
92+
_ context.Context, cause error, _ string, _ []string, payload proto.Message,
93+
) (result error) {
94+
m, ok := payload.(*errorspb.StringsPayload)
95+
if !ok || len(m.Details) < 2 {
96+
// If this ever happens, this means some version of the library
97+
// (presumably future) changed the payload type, and we're
98+
// receiving this here. In this case, give up and let
99+
// DecodeError use the opaque type.
100+
return nil
101+
}
102+
return &os.PathError{
103+
Op: m.Details[0],
104+
Path: m.Details[1],
105+
Err: cause,
106+
}
107+
}
108+
109+
func encodeLinkError(
110+
_ context.Context, err error,
111+
) (msgPrefix string, safe []string, details proto.Message) {
112+
p := err.(*os.LinkError)
113+
msg := p.Op + " " + p.Old + " " + p.New
114+
details = &errorspb.StringsPayload{
115+
Details: []string{p.Op, p.Old, p.New},
116+
}
117+
return msg, []string{p.Op}, details
118+
}
119+
120+
func decodeLinkError(
121+
_ context.Context, cause error, _ string, _ []string, payload proto.Message,
122+
) (result error) {
123+
m, ok := payload.(*errorspb.StringsPayload)
124+
if !ok || len(m.Details) < 3 {
125+
// If this ever happens, this means some version of the library
126+
// (presumably future) changed the payload type, and we're
127+
// receiving this here. In this case, give up and let
128+
// DecodeError use the opaque type.
129+
return nil
130+
}
131+
return &os.LinkError{
132+
Op: m.Details[0],
133+
Old: m.Details[1],
134+
New: m.Details[2],
135+
Err: cause,
136+
}
137+
}
138+
139+
func encodeSyscallError(
140+
_ context.Context, err error,
141+
) (msgPrefix string, safe []string, details proto.Message) {
142+
p := err.(*os.SyscallError)
143+
return p.Syscall, nil, nil
144+
}
145+
146+
func decodeSyscallError(
147+
_ context.Context, cause error, msg string, _ []string, _ proto.Message,
148+
) (result error) {
149+
return os.NewSyscallError(msg, cause)
150+
}
151+
152+
// OpaqueErrno represents a syscall.Errno error object that
153+
// was constructed on a different OS/platform combination.
154+
type OpaqueErrno struct {
155+
msg string
156+
details *errorspb.ErrnoPayload
157+
}
158+
159+
// Error implements the error interface.
160+
func (o *OpaqueErrno) Error() string { return o.msg }
161+
162+
// Is tests whether this opaque errno object represents a special os error type.
163+
func (o *OpaqueErrno) Is(target error) bool {
164+
return (target == os.ErrPermission && o.details.IsPermission) ||
165+
(target == os.ErrExist && o.details.IsExist) ||
166+
(target == os.ErrNotExist && o.details.IsNotExist)
167+
}
168+
169+
// Temporary tests whether this opaque errno object encodes a temporary error.
170+
func (o *OpaqueErrno) Temporary() bool { return o.details.IsTemporary }
171+
172+
// Timeout tests whether this opaque errno object encodes a timeout error.
173+
func (o *OpaqueErrno) Timeout() bool { return o.details.IsTimeout }
174+
175+
func encodeOpaqueErrno(
176+
_ context.Context, err error,
177+
) (msg string, safe []string, payload proto.Message) {
178+
e := err.(*OpaqueErrno)
179+
return e.Error(), []string{e.Error()}, e.details
180+
}
181+
78182
func init() {
79183
baseErr := goErr.New("")
80184
RegisterLeafDecoder(GetTypeKey(baseErr), decodeErrorString)
@@ -86,4 +190,16 @@ func init() {
86190

87191
ws := pkgErr.WithStack(baseErr)
88192
RegisterWrapperEncoder(GetTypeKey(ws), encodePkgWithStack)
193+
194+
pKey := GetTypeKey(&os.PathError{})
195+
RegisterWrapperEncoder(pKey, encodePathError)
196+
RegisterWrapperDecoder(pKey, decodePathError)
197+
pKey = GetTypeKey(&os.LinkError{})
198+
RegisterWrapperEncoder(pKey, encodeLinkError)
199+
RegisterWrapperDecoder(pKey, decodeLinkError)
200+
pKey = GetTypeKey(&os.SyscallError{})
201+
RegisterWrapperEncoder(pKey, encodeSyscallError)
202+
RegisterWrapperDecoder(pKey, decodeSyscallError)
203+
204+
RegisterLeafEncoder(GetTypeKey(&OpaqueErrno{}), encodeOpaqueErrno)
89205
}

errbase/adapters_errno.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2019 The Cockroach Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
// implied. See the License for the specific language governing
13+
// permissions and limitations under the License.
14+
15+
// +build !plan9
16+
17+
package errbase
18+
19+
import (
20+
"context"
21+
"os"
22+
"runtime"
23+
"syscall"
24+
25+
"github.com/cockroachdb/errors/errorspb"
26+
"github.com/gogo/protobuf/proto"
27+
)
28+
29+
const thisArch = runtime.GOOS + ":" + runtime.GOARCH
30+
31+
func encodeErrno(_ context.Context, err error) (msg string, safe []string, payload proto.Message) {
32+
e := err.(syscall.Errno)
33+
payload = &errorspb.ErrnoPayload{
34+
OrigErrno: int64(e),
35+
Arch: thisArch,
36+
IsPermission: e.Is(os.ErrPermission),
37+
IsExist: e.Is(os.ErrExist),
38+
IsNotExist: e.Is(os.ErrNotExist),
39+
IsTimeout: e.Timeout(),
40+
IsTemporary: e.Temporary(),
41+
}
42+
return e.Error(), []string{e.Error()}, payload
43+
}
44+
45+
func decodeErrno(_ context.Context, msg string, _ []string, payload proto.Message) error {
46+
m, ok := payload.(*errorspb.ErrnoPayload)
47+
if !ok {
48+
// If this ever happens, this means some version of the library
49+
// (presumably future) changed the payload type, and we're
50+
// receiving this here. In this case, give up and let
51+
// DecodeError use the opaque type.
52+
return nil
53+
}
54+
if m.Arch != thisArch {
55+
// The errno object is coming from a different platform. We'll
56+
// keep it opaque here.
57+
return &OpaqueErrno{msg: msg, details: m}
58+
}
59+
return syscall.Errno(m.OrigErrno)
60+
}
61+
62+
func init() {
63+
pKey := GetTypeKey(syscall.Errno(0))
64+
RegisterLeafEncoder(pKey, encodeErrno)
65+
RegisterLeafDecoder(pKey, decodeErrno)
66+
}

errbase/adapters_errno_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2019 The Cockroach Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
// implied. See the License for the specific language governing
13+
// permissions and limitations under the License.
14+
15+
// +build !plan9
16+
17+
package errbase_test
18+
19+
import (
20+
"context"
21+
"reflect"
22+
"syscall"
23+
"testing"
24+
25+
"github.com/cockroachdb/errors/errbase"
26+
"github.com/cockroachdb/errors/errorspb"
27+
"github.com/cockroachdb/errors/oserror"
28+
"github.com/cockroachdb/errors/testutils"
29+
"github.com/gogo/protobuf/types"
30+
)
31+
32+
func TestAdaptErrno(t *testing.T) {
33+
tt := testutils.T{T: t}
34+
35+
// Arbitrary values of errno on a given platform are preserved
36+
// exactly when decoded on the same platform.
37+
origErr := syscall.Errno(123)
38+
newErr := network(t, origErr)
39+
tt.Check(reflect.DeepEqual(newErr, origErr))
40+
41+
// Common values of errno preserve their properties
42+
// across a network encode/decode even though they
43+
// may not decode to the same type.
44+
for i := 0; i < 2000; i++ {
45+
origErr := syscall.Errno(i)
46+
enc := errbase.EncodeError(context.Background(), origErr)
47+
48+
// Trick the decoder into thinking the error comes from a different platform.
49+
details := &enc.Error.(*errorspb.EncodedError_Leaf).Leaf.Details
50+
var d types.DynamicAny
51+
if err := types.UnmarshalAny(details.FullDetails, &d); err != nil {
52+
t.Fatal(err)
53+
}
54+
errnoDetails := d.Message.(*errorspb.ErrnoPayload)
55+
errnoDetails.Arch = "OTHER"
56+
any, err := types.MarshalAny(errnoDetails)
57+
if err != nil {
58+
t.Fatal(err)
59+
}
60+
details.FullDetails = any
61+
62+
// Now decode the error. This produces an OpaqueErrno payload.
63+
dec := errbase.DecodeError(context.Background(), enc)
64+
if _, ok := dec.(*errbase.OpaqueErrno); !ok {
65+
t.Fatalf("expected OpaqueErrno, got %T", dec)
66+
}
67+
68+
// Now check that the properties have been preserved properly.
69+
tt.CheckEqual(oserror.IsPermission(origErr), oserror.IsPermission(dec))
70+
tt.CheckEqual(oserror.IsExist(origErr), oserror.IsExist(dec))
71+
tt.CheckEqual(oserror.IsNotExist(origErr), oserror.IsNotExist(dec))
72+
tt.CheckEqual(oserror.IsTimeout(origErr), oserror.IsTimeout(dec))
73+
}
74+
}

errbase/adapters_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
goErr "errors"
2020
"fmt"
21+
"os"
2122
"reflect"
2223
"strings"
2324
"testing"
@@ -170,3 +171,22 @@ func TestAdaptProtoErrorsWithWrapper(t *testing.T) {
170171
// Moreover, it preserves the entire structure.
171172
tt.CheckDeepEqual(newErr, origErr)
172173
}
174+
175+
func TestAdaptOsErrors(t *testing.T) {
176+
// The special os error types are preserved exactly.
177+
178+
tt := testutils.T{T: t}
179+
var origErr error
180+
181+
origErr = &os.PathError{Op: "hello", Path: "world", Err: goErr.New("woo")}
182+
newErr := network(t, origErr)
183+
tt.Check(reflect.DeepEqual(newErr, origErr))
184+
185+
origErr = &os.LinkError{Op: "hello", Old: "world", New: "universe", Err: goErr.New("woo")}
186+
newErr = network(t, origErr)
187+
tt.Check(reflect.DeepEqual(newErr, origErr))
188+
189+
origErr = &os.SyscallError{Syscall: "hello", Err: goErr.New("woo")}
190+
newErr = network(t, origErr)
191+
tt.Check(reflect.DeepEqual(newErr, origErr))
192+
}

0 commit comments

Comments
 (0)