Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cid-fmt Enhancments #61

Merged
merged 8 commits into from
Aug 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 19 additions & 168 deletions cid-fmt/main.go
Original file line number Diff line number Diff line change
@@ -1,46 +1,21 @@
package main

import (
"bytes"
"fmt"
"os"
"strings"

c "github.com/ipfs/go-cid"

mb "github.com/multiformats/go-multibase"
mh "github.com/multiformats/go-multihash"
)

func usage() {
fmt.Fprintf(os.Stderr, "usage: %s [-b multibase-code] [-v cid-version] <fmt-str> <cid> ...\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "<fmt-str> is either 'prefix' or a printf style format string:\n%s", fmtRef)
fmt.Fprintf(os.Stderr, "<fmt-str> is either 'prefix' or a printf style format string:\n%s", c.FormatRef)
os.Exit(2)
}

const fmtRef = `
%% literal %
%b multibase name
%B multibase code
%v version string
%V version number
%c codec name
%C codec code
%h multihash name
%H multihash code
%L hash digest length
%m multihash encoded in base %b (with multibase prefix)
%M multihash encoded in base %b without multibase prefix
%d hash digest encoded in base %b (with multibase prefix)
%D hash digest encoded in base %b without multibase prefix
%s cid string encoded in base %b (1)
%s cid string encoded in base %b without multibase prefix
%P cid prefix: %v-%c-%h-%L

(1) For CID version 0 the multibase must be base58btc and no prefix is
used. For Cid version 1 the multibase prefix is included.
`

func main() {
if len(os.Args) < 2 {
usage()
Expand All @@ -55,11 +30,12 @@ outer:
if len(args) < 2 {
usage()
}
if len(args[1]) != 1 {
fmt.Fprintf(os.Stderr, "Error: Invalid multibase code: %s\n", args[1])
encoder, err := mb.EncoderByName(args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
os.Exit(2)
}
newBase = mb.Encoding(args[1][0])
newBase = encoder.Encoding()
args = args[2:]
case "-v":
if len(args) < 2 {
Expand Down Expand Up @@ -93,15 +69,16 @@ outer:
}
}
for _, cidStr := range args[1:] {
base, cid, err := decode(cidStr)
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
}
if newBase != -1 {
base = newBase
base := newBase
if newBase == -1 {
base, _ = c.ExtractEncoding(cidStr)
}
if verConv != nil {
cid, err = verConv(cid)
Expand All @@ -112,11 +89,18 @@ outer:
continue
}
}
str, err := fmtCid(fmtStr, base, cid)
if err != nil {
str, err := c.Format(fmtStr, base, cid)
switch err.(type) {
case c.FormatStringError:
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
// An error here means a bad format string, no point in continuing
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)
}
Expand All @@ -132,139 +116,6 @@ func errorMsg(fmtStr string, a ...interface{}) {
exitCode = 1
}

func decode(v string) (mb.Encoding, *c.Cid, error) {
if len(v) < 2 {
return 0, nil, c.ErrCidTooShort
}

if len(v) == 46 && v[:2] == "Qm" {
hash, err := mh.FromB58String(v)
if err != nil {
return 0, nil, err
}

return mb.Base58BTC, c.NewCidV0(hash), nil
}

base, data, err := mb.Decode(v)
if err != nil {
return 0, nil, err
}

cid, err := c.Cast(data)

return base, cid, err
}

const ERR_STR = "!ERROR!"

func fmtCid(fmtStr string, base mb.Encoding, cid *c.Cid) (string, error) {
p := cid.Prefix()
out := new(bytes.Buffer)
var err error
for i := 0; i < len(fmtStr); i++ {
if fmtStr[i] != '%' {
out.WriteByte(fmtStr[i])
continue
}
i++
if i >= len(fmtStr) {
return "", fmt.Errorf("premature end of format string")
}
switch fmtStr[i] {
case '%':
out.WriteByte('%')
case 'b': // base name
out.WriteString(baseToString(base))
case 'B': // base code
out.WriteByte(byte(base))
case 'v': // version string
fmt.Fprintf(out, "cidv%d", p.Version)
case 'V': // version num
fmt.Fprintf(out, "%d", p.Version)
case 'c': // codec name
out.WriteString(codecToString(p.Codec))
case 'C': // codec code
fmt.Fprintf(out, "%d", p.Codec)
case 'h': // hash fun name
out.WriteString(hashToString(p.MhType))
case 'H': // hash fun code
fmt.Fprintf(out, "%d", p.MhType)
case 'L': // hash length
fmt.Fprintf(out, "%d", p.MhLength)
case 'm', 'M': // multihash encoded in base %b
out.WriteString(encode(base, cid.Hash(), fmtStr[i] == 'M'))
case 'd', 'D': // hash digest encoded in base %b
dec, err := mh.Decode(cid.Hash())
if err != nil {
out.WriteString(ERR_STR)
errorMsg("%v", err)
continue
}
out.WriteString(encode(base, dec.Digest, fmtStr[i] == 'D'))
case 's': // cid string encoded in base %b
str, err := cid.StringOfBase(base)
if err != nil {
out.WriteString(ERR_STR)
errorMsg("%v", err)
continue
}
out.WriteString(str)
case 'S': // cid string without base prefix
out.WriteString(encode(base, cid.Bytes(), true))
case 'P': // prefix
fmt.Fprintf(out, "cidv%d-%s-%s-%d",
p.Version,
codecToString(p.Codec),
hashToString(p.MhType),
p.MhLength,
)
default:
return "", fmt.Errorf("unrecognized specifier in format string: %c", fmtStr[i])
}

}
return out.String(), err
}

func baseToString(base mb.Encoding) string {
// FIXME: Use lookup tables when they are added to go-multibase
switch base {
case mb.Base58BTC:
return "base58btc"
default:
return fmt.Sprintf("base?%c", base)
}
}

func codecToString(num uint64) string {
name, ok := c.CodecToStr[num]
if !ok {
return fmt.Sprintf("codec?%d", num)
}
return name
}

func hashToString(num uint64) string {
name, ok := mh.Codes[num]
if !ok {
return fmt.Sprintf("hash?%d", num)
}
return name
}

func encode(base mb.Encoding, data []byte, strip bool) string {
str, err := mb.Encode(base, data)
if err != nil {
errorMsg("%v", err)
return ERR_STR
}
if strip {
return str[1:]
}
return str
}

func toCidV0(cid *c.Cid) (*c.Cid, error) {
if cid.Type() != c.DagProtobuf {
return nil, fmt.Errorf("can't convert non-protobuf nodes to cidv0")
Expand Down
70 changes: 3 additions & 67 deletions cid-fmt/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,13 @@ import (
"fmt"
"testing"

mb "github.com/multiformats/go-multibase"
c "github.com/ipfs/go-cid"
)

func TestFmt(t *testing.T) {
cids := map[string]string{
"cidv0": "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn",
"cidv1": "zdj7WfLr9DhLrb1hsoSi4fSdjjxuZmeqgEtBPWxMLtPbDNbFD",
}
tests := []struct {
cidId string
newBase mb.Encoding
fmtStr string
result string
}{
{"cidv0", -1, "%P", "cidv0-protobuf-sha2-256-32"},
{"cidv0", -1, "%b-%v-%c-%h-%L", "base58btc-cidv0-protobuf-sha2-256-32"},
{"cidv0", -1, "%s", "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"},
{"cidv0", -1, "%S", "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"},
{"cidv0", -1, "ver#%V/#%C/#%H/%L", "ver#0/#112/#18/32"},
{"cidv0", -1, "%m", "zQmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"},
{"cidv0", -1, "%M", "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"},
{"cidv0", -1, "%d", "z72gdmFAgRzYHkJzKiL8MgMMRW3BTSCGyDHroPxJbxMJn"},
{"cidv0", -1, "%D", "72gdmFAgRzYHkJzKiL8MgMMRW3BTSCGyDHroPxJbxMJn"},
{"cidv0", 'B', "%S", "CIQFTFEEHEDF6KLBT32BFAGLXEZL4UWFNWM4LFTLMXQBCERZ6CMLX3Y"},
{"cidv0", 'B', "%B%S", "BCIQFTFEEHEDF6KLBT32BFAGLXEZL4UWFNWM4LFTLMXQBCERZ6CMLX3Y"},
{"cidv1", -1, "%P", "cidv1-protobuf-sha2-256-32"},
{"cidv1", -1, "%b-%v-%c-%h-%L", "base58btc-cidv1-protobuf-sha2-256-32"},
{"cidv1", -1, "%s", "zdj7WfLr9DhLrb1hsoSi4fSdjjxuZmeqgEtBPWxMLtPbDNbFD"},
{"cidv1", -1, "%S", "dj7WfLr9DhLrb1hsoSi4fSdjjxuZmeqgEtBPWxMLtPbDNbFD"},
{"cidv1", -1, "ver#%V/#%C/#%H/%L", "ver#1/#112/#18/32"},
{"cidv1", -1, "%m", "zQmYFbmndVP7QqAVWyKhpmMuQHMaD88pkK57RgYVimmoh5H"},
{"cidv1", -1, "%M", "QmYFbmndVP7QqAVWyKhpmMuQHMaD88pkK57RgYVimmoh5H"},
{"cidv1", -1, "%d", "zAux4gVVsLRMXtsZ9fd3tFEZN4jGYB6kP37fgoZNTc11H"},
{"cidv1", -1, "%D", "Aux4gVVsLRMXtsZ9fd3tFEZN4jGYB6kP37fgoZNTc11H"},
{"cidv1", 'B', "%s", "BAFYBEIETJGSRL3EQPQPCABV3G6IUBYTSIFVQ24XRRHD3JUETSKLTGQ7DJA"},
{"cidv1", 'B', "%S", "AFYBEIETJGSRL3EQPQPCABV3G6IUBYTSIFVQ24XRRHD3JUETSKLTGQ7DJA"},
{"cidv1", 'B', "%B%S", "BAFYBEIETJGSRL3EQPQPCABV3G6IUBYTSIFVQ24XRRHD3JUETSKLTGQ7DJA"},
}
for _, tc := range tests {
name := fmt.Sprintf("%s/%s", tc.cidId, tc.fmtStr)
if tc.newBase != -1 {
name = fmt.Sprintf("%s/%c", name, tc.newBase)
}
cidStr := cids[tc.cidId]
t.Run(name, func(t *testing.T) {
testFmt(t, cidStr, tc.newBase, tc.fmtStr, tc.result)
})
}
}

func testFmt(t *testing.T, cidStr string, newBase mb.Encoding, fmtStr string, result string) {
base, cid, err := decode(cidStr)
if newBase != -1 {
base = newBase
}
if err != nil {
t.Fatal(err)
}
str, err := fmtCid(fmtStr, base, cid)
if err != nil {
t.Fatal(err)
}
if str != result {
t.Error(fmt.Sprintf("expected: %s; but got: %s", result, str))
}
}

func TestCidConv(t *testing.T) {
cidv0 := "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"
cidv1 := "zdj7WbTaiJT1fgatdet9Ei9iDB5hdCxkbVyhyh8YTUnXMiwYi"
_, cid, err := decode(cidv0)
cid, err := c.Decode(cidv0)
if err != nil {
t.Fatal(err)
}
Expand All @@ -98,7 +34,7 @@ func TestCidConv(t *testing.T) {
func TestBadCidConv(t *testing.T) {
// this cid is a raw leaf and should not be able to convert to cidv0
cidv1 := "zb2rhhzX7uSKrtQ2ZZXFAabKiKFYZrJqKY2KE1cJ8yre2GSWZ"
_, cid, err := decode(cidv1)
cid, err := c.Decode(cidv1)
if err != nil {
t.Fatal(err)
}
Expand Down
22 changes: 22 additions & 0 deletions cid.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,28 @@ func Decode(v string) (*Cid, error) {
return Cast(data)
}

// Extract the encoding from a Cid. If Decode on the same string did
// not return an error neither will this function.
func ExtractEncoding(v string) (mbase.Encoding, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about providing a "decode" function that also returns the encoding (e.g., DecodeWithEncoding)? Decode could then be modified to just call that internally.

(not a strong opinion, I just feel it would be slightly more useful/efficient as one usually wants to do both at the same time)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did that at first. The ExtractEncoding is useful when you just want the encoding and it is fairly efferent and should not allocate any heap memory so the cost of the extra function call should be negligible.

if len(v) < 2 {
return -1, ErrCidTooShort
}

if len(v) == 46 && v[:2] == "Qm" {
return mbase.Base58BTC, nil
}

encoding := mbase.Encoding(v[0])

// check encoding is valid
_, err := mbase.NewEncoder(encoding)
if err != nil {
return -1, err
}

return encoding, nil
}

func uvError(read int) error {
switch {
case read == 0:
Expand Down
Loading