This repository has been archived by the owner on Oct 5, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #156 from ipfs/fix/ipld-ErrNotFound
fix: make Block().* return correct ABI based ipld.ErrNotFound errors
- Loading branch information
Showing
6 changed files
with
519 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
package httpapi | ||
|
||
import ( | ||
"errors" | ||
"strings" | ||
"unicode/utf8" | ||
|
||
"github.com/ipfs/go-cid" | ||
ipld "github.com/ipfs/go-ipld-format" | ||
mbase "github.com/multiformats/go-multibase" | ||
) | ||
|
||
// This file handle parsing and returning the correct ABI based errors from error messages | ||
|
||
type prePostWrappedNotFoundError struct { | ||
pre string | ||
post string | ||
|
||
wrapped ipld.ErrNotFound | ||
} | ||
|
||
func (e prePostWrappedNotFoundError) String() string { | ||
return e.Error() | ||
} | ||
|
||
func (e prePostWrappedNotFoundError) Error() string { | ||
return e.pre + e.wrapped.Error() + e.post | ||
} | ||
|
||
func (e prePostWrappedNotFoundError) Unwrap() error { | ||
return e.wrapped | ||
} | ||
|
||
func parseErrNotFoundWithFallbackToMSG(msg string) error { | ||
err, handled := parseErrNotFound(msg) | ||
if handled { | ||
return err | ||
} | ||
|
||
return errors.New(msg) | ||
} | ||
|
||
func parseErrNotFoundWithFallbackToError(msg error) error { | ||
err, handled := parseErrNotFound(msg.Error()) | ||
if handled { | ||
return err | ||
} | ||
|
||
return msg | ||
} | ||
|
||
//lint:ignore ST1008 this function is not using the error as a mean to return failure but it massages it to return the correct type | ||
func parseErrNotFound(msg string) (error, bool) { | ||
if msg == "" { | ||
return nil, true // Fast path | ||
} | ||
|
||
if err, handled := parseIPLDErrNotFound(msg); handled { | ||
return err, true | ||
} | ||
|
||
if err, handled := parseBlockstoreNotFound(msg); handled { | ||
return err, true | ||
} | ||
|
||
return nil, false | ||
} | ||
|
||
// Assume CIDs break on: | ||
// - Whitespaces: " \t\n\r\v\f" | ||
// - Semicolon: ";" this is to parse ipld.ErrNotFound wrapped in multierr | ||
// - Double Quotes: "\"" this is for parsing %q and %#v formating | ||
const cidBreakSet = " \t\n\r\v\f;\"" | ||
|
||
//lint:ignore ST1008 using error as values | ||
func parseIPLDErrNotFound(msg string) (error, bool) { | ||
// The patern we search for is: | ||
const ipldErrNotFoundKey = "ipld: could not find " /*CID*/ | ||
// We try to parse the CID, if it's invalid we give up and return a simple text error. | ||
// We also accept "node" in place of the CID because that means it's an Undefined CID. | ||
|
||
keyIndex := strings.Index(msg, ipldErrNotFoundKey) | ||
|
||
if keyIndex < 0 { // Unknown error | ||
return nil, false | ||
} | ||
|
||
cidStart := keyIndex + len(ipldErrNotFoundKey) | ||
|
||
msgPostKey := msg[cidStart:] | ||
var c cid.Cid | ||
var postIndex int | ||
if strings.HasPrefix(msgPostKey, "node") { | ||
// Fallback case | ||
c = cid.Undef | ||
postIndex = len("node") | ||
} else { | ||
postIndex = strings.IndexFunc(msgPostKey, func(r rune) bool { | ||
return strings.ContainsAny(string(r), cidBreakSet) | ||
}) | ||
if postIndex < 0 { | ||
// no breakage meaning the string look like this something + "ipld: could not find bafy" | ||
postIndex = len(msgPostKey) | ||
} | ||
|
||
cidStr := msgPostKey[:postIndex] | ||
|
||
var err error | ||
c, err = cid.Decode(cidStr) | ||
if err != nil { | ||
// failed to decode CID give up | ||
return nil, false | ||
} | ||
|
||
// check that the CID is either a CIDv0 or a base32 multibase | ||
// because that what ipld.ErrNotFound.Error() -> cid.Cid.String() do currently | ||
if c.Version() != 0 { | ||
baseRune, _ := utf8.DecodeRuneInString(cidStr) | ||
if baseRune == utf8.RuneError || baseRune != mbase.Base32 { | ||
// not a multibase we expect, give up | ||
return nil, false | ||
} | ||
} | ||
} | ||
|
||
err := ipld.ErrNotFound{Cid: c} | ||
pre := msg[:keyIndex] | ||
post := msgPostKey[postIndex:] | ||
|
||
if len(pre) > 0 || len(post) > 0 { | ||
return prePostWrappedNotFoundError{ | ||
pre: pre, | ||
post: post, | ||
wrapped: err, | ||
}, true | ||
} | ||
|
||
return err, true | ||
} | ||
|
||
// This is a simple error type that just return msg as Error(). | ||
// But that also match ipld.ErrNotFound when called with Is(err). | ||
// That is needed to keep compatiblity with code that use string.Contains(err.Error(), "blockstore: block not found") | ||
// and code using ipld.ErrNotFound | ||
type blockstoreNotFoundMatchingIPLDErrNotFound struct { | ||
msg string | ||
} | ||
|
||
func (e blockstoreNotFoundMatchingIPLDErrNotFound) String() string { | ||
return e.Error() | ||
} | ||
|
||
func (e blockstoreNotFoundMatchingIPLDErrNotFound) Error() string { | ||
return e.msg | ||
} | ||
|
||
func (e blockstoreNotFoundMatchingIPLDErrNotFound) Is(err error) bool { | ||
_, ok := err.(ipld.ErrNotFound) | ||
return ok | ||
} | ||
|
||
//lint:ignore ST1008 using error as values | ||
func parseBlockstoreNotFound(msg string) (error, bool) { | ||
if !strings.Contains(msg, "blockstore: block not found") { | ||
return nil, false | ||
} | ||
|
||
return blockstoreNotFoundMatchingIPLDErrNotFound{msg: msg}, true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package httpapi | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/ipfs/go-cid" | ||
ipld "github.com/ipfs/go-ipld-format" | ||
mbase "github.com/multiformats/go-multibase" | ||
mh "github.com/multiformats/go-multihash" | ||
) | ||
|
||
var randomSha256MH = mh.Multihash{0x12, 0x20, 0x88, 0x82, 0x73, 0x37, 0x7c, 0xc1, 0xc9, 0x96, 0xad, 0xee, 0xd, 0x26, 0x84, 0x2, 0xc9, 0xc9, 0x5c, 0xf9, 0x5c, 0x4d, 0x9b, 0xc3, 0x3f, 0xfb, 0x4a, 0xd8, 0xaf, 0x28, 0x6b, 0xca, 0x1a, 0xf2} | ||
|
||
func doParseIpldNotFoundTest(t *testing.T, original error) { | ||
originalMsg := original.Error() | ||
|
||
rebuilt := parseErrNotFoundWithFallbackToMSG(originalMsg) | ||
|
||
rebuiltMsg := rebuilt.Error() | ||
|
||
if originalMsg != rebuiltMsg { | ||
t.Errorf("expected message to be %q; got %q", originalMsg, rebuiltMsg) | ||
} | ||
|
||
originalNotFound := ipld.IsNotFound(original) | ||
rebuiltNotFound := ipld.IsNotFound(rebuilt) | ||
if originalNotFound != rebuiltNotFound { | ||
t.Errorf("for %q expected Ipld.IsNotFound to be %t; got %t", originalMsg, originalNotFound, rebuiltNotFound) | ||
} | ||
} | ||
|
||
func TestParseIPLDNotFound(t *testing.T) { | ||
if err := parseErrNotFoundWithFallbackToMSG(""); err != nil { | ||
t.Errorf("expected empty string to give no error; got %T %q", err, err.Error()) | ||
} | ||
|
||
cidBreaks := make([]string, len(cidBreakSet)) | ||
for i, v := range cidBreakSet { | ||
cidBreaks[i] = "%w" + string(v) | ||
} | ||
|
||
base58BTCEncoder, err := mbase.NewEncoder(mbase.Base58BTC) | ||
if err != nil { | ||
t.Fatalf("expected to find Base58BTC encoder; got error %q", err.Error()) | ||
} | ||
|
||
for _, wrap := range append(cidBreaks, | ||
"", | ||
"merkledag: %w", | ||
"testing: %w the test", | ||
"%w is wrong", | ||
) { | ||
for _, err := range [...]error{ | ||
errors.New("ipld: could not find "), | ||
errors.New("ipld: could not find Bad_CID"), | ||
errors.New("ipld: could not find " + cid.NewCidV1(cid.Raw, randomSha256MH).Encode(base58BTCEncoder)), // Test that we only accept CIDv0 and base32 CIDs | ||
errors.New("network connection timeout"), | ||
ipld.ErrNotFound{Cid: cid.Undef}, | ||
ipld.ErrNotFound{Cid: cid.NewCidV0(randomSha256MH)}, | ||
ipld.ErrNotFound{Cid: cid.NewCidV1(cid.Raw, randomSha256MH)}, | ||
} { | ||
if wrap != "" { | ||
err = fmt.Errorf(wrap, err) | ||
} | ||
|
||
doParseIpldNotFoundTest(t, err) | ||
} | ||
} | ||
} | ||
|
||
func TestBlockstoreNotFoundMatchingIPLDErrNotFound(t *testing.T) { | ||
if !ipld.IsNotFound(blockstoreNotFoundMatchingIPLDErrNotFound{}) { | ||
t.Fatalf("expected blockstoreNotFoundMatchingIPLDErrNotFound to match ipld.IsNotFound; got false") | ||
} | ||
|
||
for _, wrap := range [...]string{ | ||
"", | ||
"merkledag: %w", | ||
"testing: %w the test", | ||
"%w is wrong", | ||
} { | ||
for _, err := range [...]error{ | ||
errors.New("network connection timeout"), | ||
blockstoreNotFoundMatchingIPLDErrNotFound{"blockstore: block not found"}, | ||
} { | ||
if wrap != "" { | ||
err = fmt.Errorf(wrap, err) | ||
} | ||
|
||
doParseIpldNotFoundTest(t, err) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.