diff --git a/.gx/lastpubver b/.gx/lastpubver index 38e7732..a19ded8 100644 --- a/.gx/lastpubver +++ b/.gx/lastpubver @@ -1 +1 @@ -0.1.2: QmbfKu17LbMWyGUxHEUns9Wf5Dkm8PT6be4uPhTkk4YvaV +0.1.4: QmckgkstbdXagMTQ4e1DW2SzxGcjjudbqEvA5H2Rb7uvAT diff --git a/apicid/apicid.go b/apicid/apicid.go new file mode 100644 index 0000000..22c4ea7 --- /dev/null +++ b/apicid/apicid.go @@ -0,0 +1,61 @@ +package apicid + +import ( + cid "github.com/ipfs/go-cid" + "github.com/ipfs/go-cidutil/cidenc" + mbase "github.com/multiformats/go-multibase" +) + +// JSONBase is the base to use when Encoding into JSON. +var JSONBase mbase.Encoder = mbase.MustNewEncoder(mbase.Base58BTC) + +// apicid.Hash is a type to respesnt a CID in the API which marshals +// as a string +type Hash struct { + str string +} + +// FromCid creates an APICid from a Cid +func FromCid(c cid.Cid) Hash { + return Hash{c.Encode(JSONBase)} +} + +// Cid converts an APICid to a CID +func (c Hash) Cid() (cid.Cid, error) { + return cid.Decode(c.str) +} + +func (c Hash) String() string { + return c.Encode(cidenc.Default) +} + +func (c Hash) Encode(enc cidenc.Interface) string { + if c.str == "" { + return "" + } + str, err := enc.Recode(c.str) + if err != nil { + return c.str + } + return str +} + +func (c *Hash) UnmarshalText(b []byte) error { + c.str = string(b) + return nil +} + +func (c Hash) MarshalText() ([]byte, error) { + return []byte(c.str), nil +} + +// Cid is type to represent a normal CID in the API which marshals +// like a normal CID i.e. ({"/": }) but may uses cidenc.Default +// for the String() to optionally upgrade a version 0 CID to version 1 +type Cid struct { + cid.Cid +} + +func (c Cid) String() string { + return cidenc.Default.Encode(c.Cid) +} diff --git a/apicid/apicid_test.go b/apicid/apicid_test.go new file mode 100644 index 0000000..b967f88 --- /dev/null +++ b/apicid/apicid_test.go @@ -0,0 +1,48 @@ +package apicid + +import ( + "encoding/json" + "testing" + + cid "github.com/ipfs/go-cid" +) + +func TestJson(t *testing.T) { + cid, _ := cid.Decode("zb2rhak9iRgDiik36KQBRr2qiCJHdyBH7YxFmw7FTdM6zo31m") + hash := FromCid(cid) + data, err := json.Marshal(hash) + if err != nil { + t.Fatal(err) + } + if string(data) != `"zb2rhak9iRgDiik36KQBRr2qiCJHdyBH7YxFmw7FTdM6zo31m"` { + t.Fatalf("json string incorrect: %s\n", data) + } + var hash2 Hash + err = json.Unmarshal(data, &hash2) + if err != nil { + t.Fatal(err) + } + if hash != hash2 { + t.Fatal("round trip failed") + } +} + +func TestJsonMap(t *testing.T) { + cid1, _ := cid.Decode("zb2rhak9iRgDiik36KQBRr2qiCJHdyBH7YxFmw7FTdM6zo31m") + cid2, _ := cid.Decode("QmRJggJREPCt7waGQKMXymrXRvrvsSiiPjgFbLK9isuM8K") + hash1 := FromCid(cid1) + hash2 := FromCid(cid2) + m := map[Hash]string{hash1: "a value", hash2: "something else"} + data, err := json.Marshal(m) + if err != nil { + t.Fatal(err) + } + m2 := map[Hash]string{} + err = json.Unmarshal(data, &m2) + if err != nil { + t.Fatal(err) + } + if len(m2) != 2 || m[hash1] != m2[hash1] || m[hash2] != m2[hash2] { + t.Fatal("round trip failed") + } +} diff --git a/cid-fmt/main.go b/cid-fmt/main.go index 3aae3ee..8735658 100644 --- a/cid-fmt/main.go +++ b/cid-fmt/main.go @@ -1,7 +1,9 @@ package main import ( + "bufio" "fmt" + "io" "os" "strings" @@ -12,7 +14,9 @@ import ( ) func usage() { - fmt.Fprintf(os.Stderr, "usage: %s [-b multibase-code] [-v cid-version] ...\n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "usage: %s [-b multibase-code] [-v cid-version] [--filter] ...\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "--filter will read from stdin and convert anything that looks like a \n") + fmt.Fprintf(os.Stderr, " -- including any non-cids that are valid Multihashes).\n") fmt.Fprintf(os.Stderr, " is either 'prefix' or a printf style format string:\n%s", cidutil.FormatRef) os.Exit(2) } @@ -24,8 +28,9 @@ func main() { newBase := mb.Encoding(-1) var verConv func(cid c.Cid) (c.Cid, error) args := os.Args[1:] + filter := false outer: - for { + for len(args) > 0 { switch args[0] { case "-b": if len(args) < 2 { @@ -52,11 +57,14 @@ outer: os.Exit(2) } args = args[2:] + case "--filter": + filter = true + args = args[1:] default: break outer } } - if len(args) < 2 { + if len(args) < 1 { usage() } fmtStr := args[0] @@ -69,41 +77,73 @@ outer: os.Exit(2) } } - for _, cidStr := range args[1:] { - cid, err := c.Decode(cidStr) - if err != nil { - fmt.Fprintf(os.Stdout, "!INVALID_CID!\n") - errorMsg("%s: %v", cidStr, err) - // Don't abort on a bad cid - continue - } + format := func(cid c.Cid, cidStr string) (string, error) { base := newBase - if newBase == -1 { + if base == -1 { base, _ = c.ExtractEncoding(cidStr) } + var err error if verConv != nil { cid, err = verConv(cid) if err != nil { - fmt.Fprintf(os.Stdout, "!ERROR!\n") - errorMsg("%s: %v", cidStr, err) - // Don't abort on a bad conversion - continue + return "", err + } + } + return cidutil.Format(fmtStr, base, cid) + } + if filter { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + buf := scanner.Bytes() + for { + i, j, cid, cidStr := cidutil.ScanForCid(buf) + os.Stdout.Write(buf[0:i]) + if i == len(buf) { + os.Stdout.Write([]byte{'\n'}) + break + } + str, err := format(cid, cidStr) + switch err.(type) { + case cidutil.FormatStringError: + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(2) + default: + // just use the orignal sting on non-fatal error + str = cidStr + case nil: + } + io.WriteString(os.Stdout, str) + buf = buf[j:] } } - str, err := cidutil.Format(fmtStr, base, cid) - switch err.(type) { - case cidutil.FormatStringError: + if err := scanner.Err(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(2) - default: - fmt.Fprintf(os.Stdout, "!ERROR!\n") - errorMsg("%s: %v", cidStr, err) - // Don't abort on cid specific errors - continue - case nil: - // no error } - fmt.Fprintf(os.Stdout, "%s\n", str) + } else { + for _, cidStr := range args[1:] { + cid, err := c.Decode(cidStr) + if err != nil { + fmt.Fprintf(os.Stdout, "!INVALID_CID!\n") + errorMsg("%s: %v", cidStr, err) + // Don't abort on a bad cid + continue + } + str, err := format(cid, cidStr) + switch err.(type) { + case cidutil.FormatStringError: + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(2) + default: + fmt.Fprintf(os.Stdout, "!ERROR!\n") + errorMsg("%s: %v", cidStr, err) + // Don't abort on cid specific errors + continue + case nil: + // no error + } + fmt.Fprintf(os.Stdout, "%s\n", str) + } } os.Exit(exitCode) } diff --git a/cidenc/encoder.go b/cidenc/encoder.go new file mode 100644 index 0000000..1310821 --- /dev/null +++ b/cidenc/encoder.go @@ -0,0 +1,85 @@ +package cidenc + +import ( + "context" + + cid "github.com/ipfs/go-cid" + mbase "github.com/multiformats/go-multibase" +) + +// Encoder is a basic Encoder that will encode Cid's using a specifed +// base and optionally upgrade a CidV0 to CidV1 +type Encoder struct { + Base mbase.Encoder + Upgrade bool +} + +// Interface is a generic interface to the Encoder functionally. +type Interface interface { + Encode(c cid.Cid) string + Recode(v string) (string, error) +} + +// Default is the default encoder +var Default = Encoder{ + Base: mbase.MustNewEncoder(mbase.Base58BTC), + Upgrade: false, +} + +func (enc Encoder) Encode(c cid.Cid) string { + if enc.Upgrade && c.Version() == 0 { + c = cid.NewCidV1(c.Type(), c.Hash()) + } + return c.Encode(enc.Base) +} + +// Recode reencodes the cid string to match the paramaters of the +// encoder +func (enc Encoder) Recode(v string) (string, error) { + skip, err := enc.noopRecode(v) + if skip || err != nil { + return v, err + } + + c, err := cid.Decode(v) + if err != nil { + return v, err + } + + return enc.Encode(c), nil +} + +func (enc Encoder) noopRecode(v string) (bool, error) { + if len(v) < 2 { + return false, cid.ErrCidTooShort + } + ver := cidVer(v) + skip := ver == 0 && !enc.Upgrade || ver == 1 && v[0] == byte(enc.Base.Encoding()) + return skip, nil +} + +func cidVer(v string) int { + if len(v) == 46 && v[:2] == "Qm" { + return 0 + } else { + return 1 + } +} + +type encoderKey struct{} + +// Enable "enables" the encoder in the context using WithValue +func Enable(ctx context.Context, enc Interface) context.Context { + return context.WithValue(ctx, encoderKey{}, enc) +} + +// Get gets an encoder from the context if it exists, otherwise the +// default context is called. +func Get(ctx context.Context) Interface { + enc, ok := ctx.Value(encoderKey{}).(Interface) + if !ok { + // FIXME: Warning? + enc = Default + } + return enc +} diff --git a/cidenc/encoder_test.go b/cidenc/encoder_test.go new file mode 100644 index 0000000..2a6829e --- /dev/null +++ b/cidenc/encoder_test.go @@ -0,0 +1,18 @@ +package cidenc + +import ( + "context" + "testing" + + mbase "github.com/multiformats/go-multibase" +) + +func TestContext(t *testing.T) { + enc := Encoder{Base: mbase.MustNewEncoder(mbase.Base64)} + ctx := context.Background() + ctx = Enable(ctx, enc) + e, ok := Get(ctx).(Encoder) + if !ok || e.Base.Encoding() != mbase.Base64 { + t.Fatal("Failed to retrive encoder from context") + } +} diff --git a/format.go b/format.go index 873d3ec..300277d 100644 --- a/format.go +++ b/format.go @@ -150,3 +150,48 @@ func encode(base mb.Encoder, data []byte, strip bool) string { } return str } + +// ScanForCid scans bytes for anything resembling a CId. If one is +// found `i` will point to the begging of the cid and `j` to to the end +// and the cid will be returned, otherwise `i` will point the end of the +// buffer and the cid will be `Undef`. +func ScanForCid(buf []byte) (i, j int, cid c.Cid, cidStr string) { + i = 0 + for { + i = j + for i < len(buf) && !asciiIsAlpha(buf[i]) { + i++ + } + j = i + if i == len(buf) { + return + } + for j < len(buf) && asciiIsAlpha(buf[j]) { + j++ + } + if j-i <= 1 || j-i > 128 || !supported[buf[i]] { + continue + } + var err error + cidStr = string(buf[i:j]) + cid, err = c.Decode(cidStr) + if err == nil { + return + } + } +} + +var supported = make([]bool, 256) + +func init() { + // for now base64 encoding are not supported as they contain non + // alhphanumeric characters + supportedPrefixes := []byte("QfFbBcCvVtThzZ") + for _, b := range supportedPrefixes { + supported[b] = true + } +} + +func asciiIsAlpha(b byte) bool { + return ('A' <= b && b <= 'Z') || ('a' <= b && b <= 'z') || ('0' <= b && b <= '9') +} diff --git a/misc.go b/misc.go new file mode 100644 index 0000000..9ea665e --- /dev/null +++ b/misc.go @@ -0,0 +1,20 @@ +package cidutil + +import ( + cid "github.com/ipfs/go-cid" + mh "github.com/multiformats/go-multihash" +) + +func TryOtherCidVersion(c cid.Cid) cid.Cid { + prefix := c.Prefix() + if prefix.Codec != cid.DagProtobuf || prefix.MhType != mh.SHA2_256 || prefix.MhLength != 32 { + return cid.Undef + } + var c1 cid.Cid + if prefix.Version == 0 { + c1 = cid.NewCidV1(cid.DagProtobuf, c.Hash()) + } else { + c1 = cid.NewCidV0(c.Hash()) + } + return c1 +} diff --git a/package.json b/package.json index 1a6b20e..87ef6e4 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,6 @@ "license": "", "name": "go-cidutil", "releaseCmd": "git commit -a -m \"gx publish $VERSION\"", - "version": "0.1.2" + "version": "0.1.4" }