Skip to content

Commit 1dc7a95

Browse files
committed
Provide new "cid" sub-command.
License: MIT Signed-off-by: Kevin Atkinson <[email protected]>
1 parent f6ba685 commit 1dc7a95

File tree

3 files changed

+309
-0
lines changed

3 files changed

+309
-0
lines changed

cmd/ipfs/ipfs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,5 @@ var cmdDetailsMap = map[string]cmdDetails{
9292
"diag/cmds": {cannotRunOnClient: true},
9393
"repo/fsck": {cannotRunOnDaemon: true},
9494
"config/edit": {cannotRunOnDaemon: true, doesNotUseRepo: true},
95+
"cid": {doesNotUseRepo: true},
9596
}

core/commands/cid.go

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"sort"
7+
"strings"
8+
"text/tabwriter"
9+
"unicode"
10+
11+
cmdkit "gx/ipfs/QmPVqQHEfLpqK7JLCsUkyam7rhuV3MAeZ9gueQQCrBwCta/go-ipfs-cmdkit"
12+
mhash "gx/ipfs/QmPnFwZ2JXKnXgMw8CdBPxn7FWh6LLdjUjxV1fKHuJnkr8/go-multihash"
13+
mbase "gx/ipfs/QmSbvata2WqNkqGtZNg8MR3SKwnB8iQ7vTPJgWqB8bC5kR/go-multibase"
14+
cmds "gx/ipfs/QmUQb3xtNzkQCgTj2NjaqcJZNv2nfSSub2QAdy9DtQMRBT/go-ipfs-cmds"
15+
cid "gx/ipfs/QmYjnkEL7i731PirfVH1sis89evN7jt4otSHw5D2xXXwUV/go-cid"
16+
verifcid "gx/ipfs/QmfMirfpEKQFctVpBYTvETxxLoU5q4ZJWsAMrtwSSE2bkn/go-verifcid"
17+
)
18+
19+
var CidCmd = &cmds.Command{
20+
Helptext: cmdkit.HelpText{
21+
Tagline: "Convert and discover properties of CIDs",
22+
},
23+
Subcommands: map[string]*cmds.Command{
24+
"format": cidFmtCmd,
25+
"base32": base32Cmd,
26+
"bases": basesCmd,
27+
"codecs": codecsCmd,
28+
"hashes": hashesCmd,
29+
},
30+
}
31+
32+
var cidFmtCmd = &cmds.Command{
33+
Helptext: cmdkit.HelpText{
34+
Tagline: "Format and convert a CID in various useful ways.",
35+
LongDescription: `
36+
Format and converts <cid>'s in various useful ways.
37+
38+
The optional format string either "prefix" or or a printf style format string:
39+
` + cid.FormatRef,
40+
},
41+
Arguments: []cmdkit.Argument{
42+
cmdkit.StringArg("cid", true, true, "Cids to format."),
43+
},
44+
Options: []cmdkit.Option{
45+
cmdkit.StringOption("f", "Format string."),
46+
cmdkit.StringOption("v", "CID version to convert to."),
47+
cmdkit.StringOption("b", "Multibase to display CID in."),
48+
},
49+
Run: func(req *cmds.Request, resp cmds.ResponseEmitter, env cmds.Environment) {
50+
fmtStr, _ := req.Options["f"].(string)
51+
verStr, _ := req.Options["v"].(string)
52+
baseStr, _ := req.Options["b"].(string)
53+
54+
opts := cidFormatOpts{}
55+
56+
switch fmtStr {
57+
case "":
58+
opts.fmtStr = "%s"
59+
case "prefix":
60+
opts.fmtStr = "%P"
61+
default:
62+
if strings.IndexByte(fmtStr, '%') == -1 {
63+
resp.SetError(fmt.Errorf("invalid format string: %s", fmtStr), cmdkit.ErrNormal)
64+
return
65+
}
66+
opts.fmtStr = fmtStr
67+
}
68+
69+
switch verStr {
70+
case "":
71+
// noop
72+
case "0":
73+
opts.verConv = toCidV0
74+
case "1":
75+
opts.verConv = toCidV1
76+
default:
77+
resp.SetError(fmt.Errorf("invalid cid version: %s\n", verStr), cmdkit.ErrNormal)
78+
return
79+
}
80+
81+
if baseStr != "" {
82+
encoder, err := mbase.EncoderByName(baseStr)
83+
if err != nil {
84+
resp.SetError(err, cmdkit.ErrNormal)
85+
return
86+
}
87+
opts.newBase = encoder.Encoding()
88+
} else {
89+
opts.newBase = mbase.Encoding(-1)
90+
}
91+
92+
res, err := formatCids(req.Arguments, opts)
93+
if err != nil {
94+
resp.SetError(err, cmdkit.ErrNormal)
95+
return
96+
}
97+
cmds.EmitOnce(resp, res)
98+
},
99+
Encoders: cmds.EncoderMap{
100+
cmds.Text: cmds.MakeEncoder(func(req *cmds.Request, w io.Writer, val interface{}) error {
101+
for _, v := range val.([]string) {
102+
fmt.Fprintf(w, "%s\n", v)
103+
}
104+
return nil
105+
}),
106+
},
107+
Type: []string{},
108+
}
109+
110+
var base32Cmd = &cmds.Command{
111+
Helptext: cmdkit.HelpText{
112+
Tagline: "Convert CIDs to Base32 CID version 1.",
113+
},
114+
Arguments: []cmdkit.Argument{
115+
cmdkit.StringArg("cid", true, true, "Cids to convert."),
116+
},
117+
Run: func(req *cmds.Request, resp cmds.ResponseEmitter, env cmds.Environment) {
118+
opts := cidFormatOpts{
119+
fmtStr: "%s",
120+
newBase: mbase.Encoding(mbase.Base32),
121+
verConv: toCidV1,
122+
}
123+
res, err := formatCids(req.Arguments, opts)
124+
if err != nil {
125+
resp.SetError(err, cmdkit.ErrNormal)
126+
return
127+
}
128+
129+
cmds.EmitOnce(resp, res)
130+
},
131+
Encoders: cidFmtCmd.Encoders,
132+
Type: cidFmtCmd.Type,
133+
}
134+
135+
type cidFormatOpts struct {
136+
fmtStr string
137+
newBase mbase.Encoding
138+
verConv func(cid *cid.Cid) (*cid.Cid, error)
139+
}
140+
141+
func formatCids(args []string, opts cidFormatOpts) ([]string, error) {
142+
var res []string
143+
for _, cidStr := range args {
144+
c, err := cid.Decode(cidStr)
145+
if err != nil {
146+
return nil, fmt.Errorf("%s: %v", cidStr, err)
147+
}
148+
base := opts.newBase
149+
if base == -1 {
150+
base, _ = cid.ExtractEncoding(cidStr)
151+
}
152+
if opts.verConv != nil {
153+
c, err = opts.verConv(c)
154+
if err != nil {
155+
return nil, fmt.Errorf("%s: %v", cidStr, err)
156+
}
157+
}
158+
str, err := cid.Format(opts.fmtStr, base, c)
159+
if _, ok := err.(cid.FormatStringError); ok {
160+
return nil, err
161+
} else if err != nil {
162+
return nil, fmt.Errorf("%s: %v", cidStr, err)
163+
}
164+
res = append(res, str)
165+
}
166+
return res, nil
167+
}
168+
169+
func toCidV0(c *cid.Cid) (*cid.Cid, error) {
170+
if c.Type() != cid.DagProtobuf {
171+
return nil, fmt.Errorf("can't convert non-protobuf nodes to cidv0")
172+
}
173+
return cid.NewCidV0(c.Hash()), nil
174+
}
175+
176+
func toCidV1(c *cid.Cid) (*cid.Cid, error) {
177+
return cid.NewCidV1(c.Type(), c.Hash()), nil
178+
}
179+
180+
type CodeAndName struct {
181+
Code int
182+
Name string
183+
}
184+
185+
var basesCmd = &cmds.Command{
186+
Helptext: cmdkit.HelpText{
187+
Tagline: "List available multibase encodings.",
188+
},
189+
Options: []cmdkit.Option{
190+
cmdkit.BoolOption("prefix", "also include the single leter prefixes in addition to the code"),
191+
cmdkit.BoolOption("numeric", "also include numeric codes"),
192+
},
193+
Run: func(req *cmds.Request, resp cmds.ResponseEmitter, env cmds.Environment) {
194+
var res []CodeAndName
195+
// use EncodingToStr in case at some point there are multiple names for a given code
196+
for code, name := range mbase.EncodingToStr {
197+
res = append(res, CodeAndName{int(code), name})
198+
}
199+
cmds.EmitOnce(resp, res)
200+
},
201+
Encoders: cmds.EncoderMap{
202+
cmds.Text: cmds.MakeEncoder(func(req *cmds.Request, w0 io.Writer, val0 interface{}) error {
203+
w := tabwriter.NewWriter(w0, 0, 0, 2, ' ', 0)
204+
prefixes, _ := req.Options["prefix"].(bool)
205+
numeric, _ := req.Options["numeric"].(bool)
206+
val := val0.([]CodeAndName)
207+
sort.Sort(multibaseSorter{val})
208+
for _, v := range val {
209+
if prefixes && v.Code >= 32 && v.Code < 127 {
210+
fmt.Fprintf(w, "%c\t", v.Code)
211+
} else if prefixes {
212+
// don't display non-printable prefixes
213+
fmt.Fprintf(w, "\t")
214+
}
215+
if numeric {
216+
fmt.Fprintf(w, "%d\t%s\n", v.Code, v.Name)
217+
} else {
218+
fmt.Fprintf(w, "%s\n", v.Name)
219+
}
220+
}
221+
w.Flush()
222+
return nil
223+
}),
224+
},
225+
Type: []CodeAndName{},
226+
}
227+
228+
var codecsCmd = &cmds.Command{
229+
Helptext: cmdkit.HelpText{
230+
Tagline: "List available CID codecs.",
231+
},
232+
Options: []cmdkit.Option{
233+
cmdkit.BoolOption("numeric", "also include numeric codes"),
234+
},
235+
Run: func(req *cmds.Request, resp cmds.ResponseEmitter, env cmds.Environment) {
236+
var res []CodeAndName
237+
// use CodecToStr as there are multiple names for a given code
238+
for code, name := range cid.CodecToStr {
239+
res = append(res, CodeAndName{int(code), name})
240+
}
241+
cmds.EmitOnce(resp, res)
242+
},
243+
Encoders: cmds.EncoderMap{
244+
cmds.Text: cmds.MakeEncoder(func(req *cmds.Request, w0 io.Writer, val0 interface{}) error {
245+
w := tabwriter.NewWriter(w0, 0, 0, 2, ' ', 0)
246+
numeric, _ := req.Options["numeric"].(bool)
247+
val := val0.([]CodeAndName)
248+
sort.Sort(codeAndNameSorter{val})
249+
for _, v := range val {
250+
if numeric {
251+
fmt.Fprintf(w, "%d\t%s\n", v.Code, v.Name)
252+
} else {
253+
fmt.Fprintf(w, "%s\n", v.Name)
254+
}
255+
}
256+
w.Flush()
257+
return nil
258+
}),
259+
},
260+
Type: []CodeAndName{},
261+
}
262+
263+
var hashesCmd = &cmds.Command{
264+
Helptext: cmdkit.HelpText{
265+
Tagline: "List available multihashes.",
266+
},
267+
Options: codecsCmd.Options,
268+
Run: func(req *cmds.Request, resp cmds.ResponseEmitter, env cmds.Environment) {
269+
var res []CodeAndName
270+
// use mhash.Codes in case at some point there are multiple names for a given code
271+
for code, name := range mhash.Codes {
272+
if !verifcid.IsGoodHash(code) {
273+
continue
274+
}
275+
res = append(res, CodeAndName{int(code), name})
276+
}
277+
cmds.EmitOnce(resp, res)
278+
},
279+
Encoders: codecsCmd.Encoders,
280+
Type: codecsCmd.Type,
281+
}
282+
283+
type multibaseSorter struct {
284+
data []CodeAndName
285+
}
286+
287+
func (s multibaseSorter) Len() int { return len(s.data) }
288+
func (s multibaseSorter) Swap(i, j int) { s.data[i], s.data[j] = s.data[j], s.data[i] }
289+
290+
func (s multibaseSorter) Less(i, j int) bool {
291+
a := unicode.ToLower(rune(s.data[i].Code))
292+
b := unicode.ToLower(rune(s.data[j].Code))
293+
if a != b {
294+
return a < b
295+
}
296+
// lowecase letters should come before uppercase
297+
return s.data[i].Code > s.data[j].Code
298+
}
299+
300+
type codeAndNameSorter struct {
301+
data []CodeAndName
302+
}
303+
304+
func (s codeAndNameSorter) Len() int { return len(s.data) }
305+
func (s codeAndNameSorter) Swap(i, j int) { s.data[i], s.data[j] = s.data[j], s.data[i] }
306+
func (s codeAndNameSorter) Less(i, j int) bool { return s.data[i].Code < s.data[j].Code }

core/commands/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ TOOL COMMANDS
6767
version Show ipfs version information
6868
update Download and apply go-ipfs updates
6969
commands List all available commands
70+
cid Convert and discover properties of CIDs
7071
7172
Use 'ipfs <command> --help' to learn more about each command.
7273
@@ -139,6 +140,7 @@ var rootSubcommands = map[string]*cmds.Command{
139140
"urlstore": urlStoreCmd,
140141
"version": lgc.NewCommand(VersionCmd),
141142
"shutdown": daemonShutdownCmd,
143+
"cid": CidCmd,
142144
}
143145

144146
// RootRO is the readonly version of Root

0 commit comments

Comments
 (0)