Commit c4bfcd06 authored by Kevin Atkinson's avatar Kevin Atkinson

Extract most of `cid-fmt` logic so it can be used as a library.

parent 36bab487
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"os" "os"
"strings" "strings"
...@@ -9,38 +8,14 @@ import ( ...@@ -9,38 +8,14 @@ import (
c "github.com/ipfs/go-cid" c "github.com/ipfs/go-cid"
mb "github.com/multiformats/go-multibase" mb "github.com/multiformats/go-multibase"
mh "github.com/multiformats/go-multihash"
) )
func usage() { 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, "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) 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() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
usage() usage()
...@@ -94,7 +69,7 @@ outer: ...@@ -94,7 +69,7 @@ outer:
} }
} }
for _, cidStr := range args[1:] { for _, cidStr := range args[1:] {
base, cid, err := decode(cidStr) base, cid, err := c.DecodeV2(cidStr)
if err != nil { if err != nil {
fmt.Fprintf(os.Stdout, "!INVALID_CID!\n") fmt.Fprintf(os.Stdout, "!INVALID_CID!\n")
errorMsg("%s: %v", cidStr, err) errorMsg("%s: %v", cidStr, err)
...@@ -113,9 +88,9 @@ outer: ...@@ -113,9 +88,9 @@ outer:
continue continue
} }
} }
str, err := fmtCid(fmtStr, base, cid) str, err := c.Format(fmtStr, base, cid)
switch err.(type) { switch err.(type) {
case FormatStringError: case c.FormatStringError:
fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(2) os.Exit(2)
default: default:
...@@ -140,146 +115,6 @@ func errorMsg(fmtStr string, a ...interface{}) { ...@@ -140,146 +115,6 @@ func errorMsg(fmtStr string, a ...interface{}) {
exitCode = 1 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!"
type FormatStringError struct {
Message string
Specifier string
}
func (e FormatStringError) Error() string {
if e.Specifier == "" {
return e.Message
} else {
return fmt.Sprintf("%s: %s", e.Message, e.Specifier)
}
}
func fmtCid(fmtStr string, base mb.Encoding, cid *c.Cid) (string, error) {
p := cid.Prefix()
out := new(bytes.Buffer)
var err error
encoder, err := mb.NewEncoder(base)
if err != nil {
return "", err
}
for i := 0; i < len(fmtStr); i++ {
if fmtStr[i] != '%' {
out.WriteByte(fmtStr[i])
continue
}
i++
if i >= len(fmtStr) {
return "", FormatStringError{"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(encoder, cid.Hash(), fmtStr[i] == 'M'))
case 'd', 'D': // hash digest encoded in base %b
dec, err := mh.Decode(cid.Hash())
if err != nil {
return "", err
}
out.WriteString(encode(encoder, dec.Digest, fmtStr[i] == 'D'))
case 's': // cid string encoded in base %b
str, err := cid.StringOfBase(base)
if err != nil {
return "", err
}
out.WriteString(str)
case 'S': // cid string without base prefix
out.WriteString(encode(encoder, 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 "", FormatStringError{"unrecognized specifier in format string", fmtStr[i-1 : i+1]}
}
}
return out.String(), err
}
func baseToString(base mb.Encoding) string {
baseStr, ok := mb.EncodingToStr[base]
if !ok {
return fmt.Sprintf("base?%c", base)
}
return baseStr
}
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.Encoder, data []byte, strip bool) string {
str := base.Encode(data)
if strip {
return str[1:]
}
return str
}
func toCidV0(cid *c.Cid) (*c.Cid, error) { func toCidV0(cid *c.Cid) (*c.Cid, error) {
if cid.Type() != c.DagProtobuf { if cid.Type() != c.DagProtobuf {
return nil, fmt.Errorf("can't convert non-protobuf nodes to cidv0") return nil, fmt.Errorf("can't convert non-protobuf nodes to cidv0")
......
...@@ -4,77 +4,13 @@ import ( ...@@ -4,77 +4,13 @@ import (
"fmt" "fmt"
"testing" "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) { func TestCidConv(t *testing.T) {
cidv0 := "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn" cidv0 := "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"
cidv1 := "zdj7WbTaiJT1fgatdet9Ei9iDB5hdCxkbVyhyh8YTUnXMiwYi" cidv1 := "zdj7WbTaiJT1fgatdet9Ei9iDB5hdCxkbVyhyh8YTUnXMiwYi"
_, cid, err := decode(cidv0) _, cid, err := c.DecodeV2(cidv0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -98,7 +34,7 @@ func TestCidConv(t *testing.T) { ...@@ -98,7 +34,7 @@ func TestCidConv(t *testing.T) {
func TestBadCidConv(t *testing.T) { func TestBadCidConv(t *testing.T) {
// this cid is a raw leaf and should not be able to convert to cidv0 // this cid is a raw leaf and should not be able to convert to cidv0
cidv1 := "zb2rhhzX7uSKrtQ2ZZXFAabKiKFYZrJqKY2KE1cJ8yre2GSWZ" cidv1 := "zb2rhhzX7uSKrtQ2ZZXFAabKiKFYZrJqKY2KE1cJ8yre2GSWZ"
_, cid, err := decode(cidv1) _, cid, err := c.DecodeV2(cidv1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
......
...@@ -213,25 +213,34 @@ func Parse(v interface{}) (*Cid, error) { ...@@ -213,25 +213,34 @@ func Parse(v interface{}) (*Cid, error) {
// starting with "Qm" are considered CidV0 and treated directly // starting with "Qm" are considered CidV0 and treated directly
// as B58-encoded multihashes. // as B58-encoded multihashes.
func Decode(v string) (*Cid, error) { func Decode(v string) (*Cid, error) {
_, cid, err := DecodeV2(v)
return cid, err
}
// DecodeV2 is like Decide but also returns the Multibase encoding the
// Cid was encoded in. EXPERIMENTAL and interface may change at any time.
func DecodeV2(v string) (mbase.Encoding, *Cid, error) {
if len(v) < 2 { if len(v) < 2 {
return nil, ErrCidTooShort return 0, nil, ErrCidTooShort
} }
if len(v) == 46 && v[:2] == "Qm" { if len(v) == 46 && v[:2] == "Qm" {
hash, err := mh.FromB58String(v) hash, err := mh.FromB58String(v)
if err != nil { if err != nil {
return nil, err return 0, nil, err
} }
return NewCidV0(hash), nil return mbase.Base58BTC, NewCidV0(hash), nil
} }
_, data, err := mbase.Decode(v) base, data, err := mbase.Decode(v)
if err != nil { if err != nil {
return nil, err return 0, nil, err
} }
return Cast(data) cid, err := Cast(data)
return base, cid, err
} }
func uvError(read int) error { func uvError(read int) error {
......
package cid
import (
"bytes"
"fmt"
mb "github.com/multiformats/go-multibase"
mh "github.com/multiformats/go-multihash"
)
// FormatRef is a string documenting the format string for the Format function
const FormatRef = `
%% 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.
`
// Format formats a cid according to the format specificer as
// documented in the FormatRef constant
func Format(fmtStr string, base mb.Encoding, cid *Cid) (string, error) {
p := cid.Prefix()
out := new(bytes.Buffer)
var err error
encoder, err := mb.NewEncoder(base)
if err != nil {
return "", err
}
for i := 0; i < len(fmtStr); i++ {
if fmtStr[i] != '%' {
out.WriteByte(fmtStr[i])
continue
}
i++
if i >= len(fmtStr) {
return "", FormatStringError{"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(encoder, cid.Hash(), fmtStr[i] == 'M'))
case 'd', 'D': // hash digest encoded in base %b
dec, err := mh.Decode(cid.Hash())
if err != nil {
return "", err
}
out.WriteString(encode(encoder, dec.Digest, fmtStr[i] == 'D'))
case 's': // cid string encoded in base %b
str, err := cid.StringOfBase(base)
if err != nil {
return "", err
}
out.WriteString(str)
case 'S': // cid string without base prefix
out.WriteString(encode(encoder, 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 "", FormatStringError{"unrecognized specifier in format string", fmtStr[i-1 : i+1]}
}
}
return out.String(), err
}
// FormatStringError is the error return from Format when the format
// string is ill formed
type FormatStringError struct {
Message string
Specifier string
}
func (e FormatStringError) Error() string {
if e.Specifier == "" {
return e.Message
} else {
return fmt.Sprintf("%s: %s", e.Message, e.Specifier)
}
}
func baseToString(base mb.Encoding) string {
baseStr, ok := mb.EncodingToStr[base]
if !ok {
return fmt.Sprintf("base?%c", base)
}
return baseStr
}
func codecToString(num uint64) string {
name, ok := 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.Encoder, data []byte, strip bool) string {
str := base.Encode(data)
if strip {
return str[1:]
}
return str
}
package cid
import (
"fmt"
"testing"
mb "github.com/multiformats/go-multibase"
)
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 := DecodeV2(cidStr)
if newBase != -1 {
base = newBase
}
if err != nil {
t.Fatal(err)
}
str, err := Format(fmtStr, base, cid)
if err != nil {
t.Fatal(err)
}
if str != result {
t.Error(fmt.Sprintf("expected: %s; but got: %s", result, str))
}
}
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