master
go 603 lines 16.5 KB
Raw
1 package commands
2
3 import (
4 "cmp"
5 "encoding/hex"
6 "errors"
7 "fmt"
8 "io"
9 "slices"
10 "strings"
11 "unicode"
12
13 verifcid "github.com/ipfs/boxo/verifcid"
14 cid "github.com/ipfs/go-cid"
15 cidutil "github.com/ipfs/go-cidutil"
16 cmds "github.com/ipfs/go-ipfs-cmds"
17 ipldmulticodec "github.com/ipld/go-ipld-prime/multicodec"
18 peer "github.com/libp2p/go-libp2p/core/peer"
19 mbase "github.com/multiformats/go-multibase"
20 mc "github.com/multiformats/go-multicodec"
21 mhash "github.com/multiformats/go-multihash"
22 )
23
24 var CidCmd = &cmds.Command{
25 Helptext: cmds.HelpText{
26 Tagline: "Convert and discover properties of CIDs",
27 },
28 Subcommands: map[string]*cmds.Command{
29 "inspect": inspectCmd,
30 "format": cidFmtCmd,
31 "base32": base32Cmd,
32 "bases": basesCmd,
33 "codecs": codecsCmd,
34 "hashes": hashesCmd,
35 },
36 Extra: CreateCmdExtras(SetDoesNotUseRepo(true)),
37 }
38
39 const (
40 cidFormatOptionName = "f"
41 cidToVersionOptionName = "v"
42 cidCodecOptionName = "mc"
43 cidMultibaseOptionName = "b"
44 )
45
46 var cidFmtCmd = &cmds.Command{
47 Helptext: cmds.HelpText{
48 Tagline: "Format and convert a CID in various useful ways.",
49 LongDescription: `
50 Format and converts <cid>'s in various useful ways.
51
52 For a human-readable breakdown of a CID, see 'ipfs cid inspect'.
53
54 The optional format string is a printf style format string:
55 ` + cidutil.FormatRef,
56 },
57 Arguments: []cmds.Argument{
58 cmds.StringArg("cid", true, true, "CIDs to format.").EnableStdin(),
59 },
60 Options: []cmds.Option{
61 cmds.StringOption(cidFormatOptionName, "Printf style format string.").WithDefault("%s"),
62 cmds.StringOption(cidToVersionOptionName, "CID version to convert to."),
63 cmds.StringOption(cidCodecOptionName, "CID multicodec to convert to."),
64 cmds.StringOption(cidMultibaseOptionName, "Multibase to display CID in."),
65 },
66 Run: func(req *cmds.Request, resp cmds.ResponseEmitter, env cmds.Environment) error {
67 fmtStr, _ := req.Options[cidFormatOptionName].(string)
68 verStr, _ := req.Options[cidToVersionOptionName].(string)
69 codecStr, _ := req.Options[cidCodecOptionName].(string)
70 baseStr, _ := req.Options[cidMultibaseOptionName].(string)
71
72 opts := cidFormatOpts{}
73
74 if strings.IndexByte(fmtStr, '%') == -1 {
75 return fmt.Errorf("invalid format string: %q", fmtStr)
76 }
77 opts.fmtStr = fmtStr
78
79 if codecStr != "" {
80 var codec mc.Code
81 err := codec.Set(codecStr)
82 if err != nil {
83 return err
84 }
85 opts.newCodec = uint64(codec)
86 } // otherwise, leave it as 0 (not a valid IPLD codec)
87
88 switch verStr {
89 case "":
90 if baseStr != "" {
91 opts.verConv = toCidV1
92 }
93 case "0":
94 if opts.newCodec != 0 && opts.newCodec != cid.DagProtobuf {
95 return errors.New("cannot convert to CIDv0 with any codec other than dag-pb")
96 }
97 if baseStr != "" && baseStr != "base58btc" {
98 return errors.New("cannot convert to CIDv0 with any multibase other than the implicit base58btc")
99 }
100 opts.verConv = toCidV0
101 case "1":
102 opts.verConv = toCidV1
103 default:
104 return fmt.Errorf("invalid cid version: %q", verStr)
105 }
106
107 if baseStr != "" {
108 encoder, err := mbase.EncoderByName(baseStr)
109 if err != nil {
110 return err
111 }
112 opts.newBase = encoder.Encoding()
113 } else {
114 opts.newBase = mbase.Encoding(-1)
115 }
116
117 return emitCids(req, resp, opts)
118 },
119 PostRun: cmds.PostRunMap{
120 cmds.CLI: streamResult(func(v any, out io.Writer) nonFatalError {
121 r := v.(*CidFormatRes)
122 if r.ErrorMsg != "" {
123 return nonFatalError(fmt.Sprintf("%s: %s", r.CidStr, r.ErrorMsg))
124 }
125 fmt.Fprintf(out, "%s\n", r.Formatted)
126 return ""
127 }),
128 },
129 Type: CidFormatRes{},
130 Extra: CreateCmdExtras(SetDoesNotUseRepo(true)),
131 }
132
133 type CidFormatRes struct {
134 CidStr string // Original Cid String passed in
135 Formatted string // Formatted Result
136 ErrorMsg string // Error
137 }
138
139 var base32Cmd = &cmds.Command{
140 Helptext: cmds.HelpText{
141 Tagline: "Convert CIDs to Base32 CID version 1.",
142 ShortDescription: `
143 'ipfs cid base32' normalizes passed CIDs to their canonical case-insensitive encoding.
144 Useful when processing third-party CIDs which could come with arbitrary formats.
145 `,
146 },
147 Arguments: []cmds.Argument{
148 cmds.StringArg("cid", true, true, "CIDs to convert.").EnableStdin(),
149 },
150 Run: func(req *cmds.Request, resp cmds.ResponseEmitter, env cmds.Environment) error {
151 opts := cidFormatOpts{
152 fmtStr: "%s",
153 newBase: mbase.Encoding(mbase.Base32),
154 verConv: toCidV1,
155 }
156 return emitCids(req, resp, opts)
157 },
158 PostRun: cidFmtCmd.PostRun,
159 Type: cidFmtCmd.Type,
160 Extra: CreateCmdExtras(SetDoesNotUseRepo(true)),
161 }
162
163 type cidFormatOpts struct {
164 fmtStr string
165 newBase mbase.Encoding
166 verConv func(cid cid.Cid) (cid.Cid, error)
167 newCodec uint64
168 }
169
170 type argumentIterator struct {
171 args []string
172 body cmds.StdinArguments
173 }
174
175 func (i *argumentIterator) next() (string, bool) {
176 if len(i.args) > 0 {
177 arg := i.args[0]
178 i.args = i.args[1:]
179 return arg, true
180 }
181 if i.body == nil || !i.body.Scan() {
182 return "", false
183 }
184 return strings.TrimSpace(i.body.Argument()), true
185 }
186
187 func (i *argumentIterator) err() error {
188 if i.body == nil {
189 return nil
190 }
191 return i.body.Err()
192 }
193
194 func emitCids(req *cmds.Request, resp cmds.ResponseEmitter, opts cidFormatOpts) error {
195 itr := argumentIterator{req.Arguments, req.BodyArgs()}
196 var emitErr error
197 for emitErr == nil {
198 cidStr, ok := itr.next()
199 if !ok {
200 break
201 }
202 res := &CidFormatRes{CidStr: cidStr}
203 c, err := cid.Decode(cidStr)
204 if err != nil {
205 res.ErrorMsg = err.Error()
206 emitErr = resp.Emit(res)
207 continue
208 }
209
210 if opts.newCodec != 0 && opts.newCodec != c.Type() {
211 c = cid.NewCidV1(opts.newCodec, c.Hash())
212 }
213
214 if opts.verConv != nil {
215 c, err = opts.verConv(c)
216 if err != nil {
217 res.ErrorMsg = err.Error()
218 emitErr = resp.Emit(res)
219 continue
220 }
221 }
222
223 base := opts.newBase
224 if base == -1 {
225 if c.Version() == 0 {
226 base = mbase.Base58BTC
227 } else {
228 base, _ = cid.ExtractEncoding(cidStr)
229 }
230 }
231
232 str, err := cidutil.Format(opts.fmtStr, base, c)
233 if _, ok := err.(cidutil.FormatStringError); ok {
234 // no point in continuing if there is a problem with the format string
235 return err
236 }
237 if err != nil {
238 res.ErrorMsg = err.Error()
239 } else {
240 res.Formatted = str
241 }
242 emitErr = resp.Emit(res)
243 }
244 if emitErr != nil {
245 return emitErr
246 }
247 err := itr.err()
248 if err != nil {
249 return err
250 }
251 return nil
252 }
253
254 func toCidV0(c cid.Cid) (cid.Cid, error) {
255 if c.Type() != cid.DagProtobuf {
256 return cid.Cid{}, fmt.Errorf("can't convert non-dag-pb nodes to cidv0")
257 }
258 return cid.NewCidV0(c.Hash()), nil
259 }
260
261 func toCidV1(c cid.Cid) (cid.Cid, error) {
262 return cid.NewCidV1(c.Type(), c.Hash()), nil
263 }
264
265 type CodeAndName struct {
266 Code int
267 Name string
268 }
269
270 const (
271 prefixOptionName = "prefix"
272 numericOptionName = "numeric"
273 )
274
275 var basesCmd = &cmds.Command{
276 Helptext: cmds.HelpText{
277 Tagline: "List available multibase encodings.",
278 ShortDescription: `
279 'ipfs cid bases' relies on https://github.com/multiformats/go-multibase
280 `,
281 },
282 Options: []cmds.Option{
283 cmds.BoolOption(prefixOptionName, "also include the single letter prefixes in addition to the code"),
284 cmds.BoolOption(numericOptionName, "also include numeric codes"),
285 },
286 Run: func(req *cmds.Request, resp cmds.ResponseEmitter, env cmds.Environment) error {
287 var res []CodeAndName
288 // use EncodingToStr in case at some point there are multiple names for a given code
289 for code, name := range mbase.EncodingToStr {
290 res = append(res, CodeAndName{int(code), name})
291 }
292 return cmds.EmitOnce(resp, res)
293 },
294 Encoders: cmds.EncoderMap{
295 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, val []CodeAndName) error {
296 prefixes, _ := req.Options[prefixOptionName].(bool)
297 numeric, _ := req.Options[numericOptionName].(bool)
298 multibaseSorter{val}.Sort()
299 for _, v := range val {
300 code := v.Code
301 if !unicode.IsPrint(rune(code)) {
302 // don't display non-printable prefixes
303 code = ' '
304 }
305 switch {
306 case prefixes && numeric:
307 fmt.Fprintf(w, "%c %7d %s\n", code, v.Code, v.Name)
308 case prefixes:
309 fmt.Fprintf(w, "%c %s\n", code, v.Name)
310 case numeric:
311 fmt.Fprintf(w, "%7d %s\n", v.Code, v.Name)
312 default:
313 fmt.Fprintf(w, "%s\n", v.Name)
314 }
315 }
316 return nil
317 }),
318 },
319 Type: []CodeAndName{},
320 Extra: CreateCmdExtras(SetDoesNotUseRepo(true)),
321 }
322
323 const (
324 codecsNumericOptionName = "numeric"
325 codecsSupportedOptionName = "supported"
326 )
327
328 var codecsCmd = &cmds.Command{
329 Helptext: cmds.HelpText{
330 Tagline: "List available CID multicodecs.",
331 ShortDescription: `
332 'ipfs cid codecs' relies on https://github.com/multiformats/go-multicodec
333 `,
334 },
335 Options: []cmds.Option{
336 cmds.BoolOption(codecsNumericOptionName, "n", "also include numeric codes"),
337 cmds.BoolOption(codecsSupportedOptionName, "s", "list only codecs supported by go-ipfs commands"),
338 },
339 Run: func(req *cmds.Request, resp cmds.ResponseEmitter, env cmds.Environment) error {
340 listSupported, _ := req.Options[codecsSupportedOptionName].(bool)
341 supportedCodecs := make(map[uint64]struct{})
342 if listSupported {
343 for _, code := range ipldmulticodec.ListEncoders() {
344 supportedCodecs[code] = struct{}{}
345 }
346 for _, code := range ipldmulticodec.ListDecoders() {
347 supportedCodecs[code] = struct{}{}
348 }
349 // add libp2p-key
350 supportedCodecs[uint64(mc.Libp2pKey)] = struct{}{}
351 }
352
353 var res []CodeAndName
354 for _, code := range mc.KnownCodes() {
355 if code.Tag() == "ipld" {
356 if listSupported {
357 if _, ok := supportedCodecs[uint64(code)]; !ok {
358 continue
359 }
360 }
361 res = append(res, CodeAndName{int(code), mc.Code(code).String()})
362 }
363 }
364 return cmds.EmitOnce(resp, res)
365 },
366 Encoders: cmds.EncoderMap{
367 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, val []CodeAndName) error {
368 numeric, _ := req.Options[codecsNumericOptionName].(bool)
369 codeAndNameSorter{val}.Sort()
370 for _, v := range val {
371 if numeric {
372 fmt.Fprintf(w, "%5d %s\n", v.Code, v.Name)
373 } else {
374 fmt.Fprintf(w, "%s\n", v.Name)
375 }
376 }
377 return nil
378 }),
379 },
380 Type: []CodeAndName{},
381 Extra: CreateCmdExtras(SetDoesNotUseRepo(true)),
382 }
383
384 var hashesCmd = &cmds.Command{
385 Helptext: cmds.HelpText{
386 Tagline: "List available multihashes.",
387 ShortDescription: `
388 'ipfs cid hashes' relies on https://github.com/multiformats/go-multihash
389 `,
390 },
391 Options: codecsCmd.Options,
392 Run: func(req *cmds.Request, resp cmds.ResponseEmitter, env cmds.Environment) error {
393 var res []CodeAndName
394 // use mhash.Codes in case at some point there are multiple names for a given code
395 for code, name := range mhash.Codes {
396 if !verifcid.DefaultAllowlist.IsAllowed(code) {
397 continue
398 }
399 res = append(res, CodeAndName{int(code), name})
400 }
401 return cmds.EmitOnce(resp, res)
402 },
403 Encoders: codecsCmd.Encoders,
404 Type: codecsCmd.Type,
405 Extra: CreateCmdExtras(SetDoesNotUseRepo(true)),
406 }
407
408 // CidInspectRes represents the response from the inspect command.
409 type CidInspectRes struct {
410 Cid string `json:"cid"`
411 Version int `json:"version"`
412 Multibase CidInspectBase `json:"multibase"`
413 Multicodec CidInspectCodec `json:"multicodec"`
414 Multihash CidInspectHash `json:"multihash"`
415 CidV0 string `json:"cidV0,omitempty"`
416 CidV1 string `json:"cidV1"`
417 ErrorMsg string `json:"errorMsg,omitempty"`
418 }
419
420 type CidInspectBase struct {
421 Prefix string `json:"prefix"`
422 Name string `json:"name"`
423 }
424
425 type CidInspectCodec struct {
426 Code uint64 `json:"code"`
427 Name string `json:"name"`
428 }
429
430 type CidInspectHash struct {
431 Code uint64 `json:"code"`
432 Name string `json:"name"`
433 Length int `json:"length"`
434 Digest string `json:"digest"`
435 }
436
437 var inspectCmd = &cmds.Command{
438 Helptext: cmds.HelpText{
439 Tagline: "Inspect and display detailed information about a CID.",
440 ShortDescription: `
441 'ipfs cid inspect' breaks down a CID and displays its components:
442 - CID version (0 or 1)
443 - Multibase encoding (explicit for CIDv1, implicit for CIDv0)
444 - Multicodec (DAG type)
445 - Multihash (hash algorithm, length, and digest)
446 - Equivalent CIDv0 and CIDv1 representations
447
448 For CIDv0, multibase, multicodec, and multihash are marked as
449 implicit because they are not explicitly encoded in the binary.
450
451 If a PeerID string is provided instead of a CID, a helpful error
452 with the equivalent CID representation is returned.
453
454 Use --enc=json for machine-readable output same as the HTTP RPC API.
455 `,
456 },
457 Arguments: []cmds.Argument{
458 cmds.StringArg("cid", true, false, "CID to inspect.").EnableStdin(),
459 },
460 Run: func(req *cmds.Request, resp cmds.ResponseEmitter, env cmds.Environment) error {
461 cidStr := req.Arguments[0]
462
463 c, err := cid.Decode(cidStr)
464 if err != nil {
465 errMsg := fmt.Sprintf("invalid CID: %s", err)
466 // PeerID fallback: try peer.Decode for legacy PeerIDs (12D3KooW..., Qm...)
467 if pid, pidErr := peer.Decode(cidStr); pidErr == nil {
468 pidCid := peer.ToCid(pid)
469 cidV1, _ := pidCid.StringOfBase(mbase.Base36)
470 errMsg += fmt.Sprintf("\nNote: the value is a PeerID; inspect its CID representation instead:\n %s", cidV1)
471 }
472 return cmds.EmitOnce(resp, &CidInspectRes{Cid: cidStr, ErrorMsg: errMsg})
473 }
474
475 res := &CidInspectRes{
476 Cid: cidStr,
477 Version: int(c.Version()),
478 }
479
480 // Multibase: always populated; CIDv0 uses implicit base58btc
481 if c.Version() == 0 {
482 res.Multibase = CidInspectBase{Prefix: "z", Name: "base58btc"}
483 } else {
484 baseCode, _ := cid.ExtractEncoding(cidStr)
485 res.Multibase = CidInspectBase{
486 Prefix: string(rune(baseCode)),
487 Name: mbase.EncodingToStr[baseCode],
488 }
489 }
490
491 // Multicodec
492 codecName := mc.Code(c.Type()).String()
493 if codecName == "" || strings.HasPrefix(codecName, "Code(") {
494 codecName = "unknown"
495 }
496 res.Multicodec = CidInspectCodec{Code: c.Type(), Name: codecName}
497
498 // Multihash
499 dmh, err := mhash.Decode(c.Hash())
500 if err != nil {
501 return cmds.EmitOnce(resp, &CidInspectRes{
502 Cid: cidStr,
503 ErrorMsg: fmt.Sprintf("failed to decode multihash: %s", err),
504 })
505 }
506 hashName := mhash.Codes[dmh.Code]
507 if hashName == "" {
508 hashName = "unknown"
509 }
510 res.Multihash = CidInspectHash{
511 Code: dmh.Code,
512 Name: hashName,
513 Length: dmh.Length,
514 Digest: hex.EncodeToString(dmh.Digest),
515 }
516
517 // CIDv0: only possible with dag-pb + sha2-256-256
518 if c.Type() == cid.DagProtobuf && dmh.Code == mhash.SHA2_256 && dmh.Length == 32 {
519 res.CidV0 = cid.NewCidV0(c.Hash()).String()
520 }
521
522 // CIDv1: use base36 for libp2p-key, base32 for everything else
523 v1 := cid.NewCidV1(c.Type(), c.Hash())
524 v1Base := mbase.Encoding(mbase.Base32)
525 if c.Type() == uint64(mc.Libp2pKey) {
526 v1Base = mbase.Base36
527 }
528 v1Str, err := v1.StringOfBase(v1Base)
529 if err != nil {
530 v1Str = v1.String()
531 }
532 res.CidV1 = v1Str
533
534 return cmds.EmitOnce(resp, res)
535 },
536 Encoders: cmds.EncoderMap{
537 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, res *CidInspectRes) error {
538 if res.ErrorMsg != "" {
539 return fmt.Errorf("%s", res.ErrorMsg)
540 }
541
542 implicit := ""
543 if res.Version == 0 {
544 implicit = ", implicit"
545 }
546
547 fmt.Fprintf(w, "CID: %s\n", res.Cid)
548 fmt.Fprintf(w, "Version: %d\n", res.Version)
549 if res.Version == 0 {
550 fmt.Fprintf(w, "Multibase: %s (implicit)\n", res.Multibase.Name)
551 } else {
552 fmt.Fprintf(w, "Multibase: %s (%s)\n", res.Multibase.Name, res.Multibase.Prefix)
553 }
554 fmt.Fprintf(w, "Multicodec: %s (0x%x%s)\n", res.Multicodec.Name, res.Multicodec.Code, implicit)
555 fmt.Fprintf(w, "Multihash: %s (0x%x%s)\n", res.Multihash.Name, res.Multihash.Code, implicit)
556 fmt.Fprintf(w, " Length: %d bytes\n", res.Multihash.Length)
557 fmt.Fprintf(w, " Digest: %s\n", res.Multihash.Digest)
558
559 if res.CidV0 != "" {
560 fmt.Fprintf(w, "CIDv0: %s\n", res.CidV0)
561 } else if res.Multicodec.Code != cid.DagProtobuf {
562 fmt.Fprintf(w, "CIDv0: not possible, requires dag-pb (0x70), got %s (0x%x)\n",
563 res.Multicodec.Name, res.Multicodec.Code)
564 } else if res.Multihash.Code != mhash.SHA2_256 {
565 fmt.Fprintf(w, "CIDv0: not possible, requires sha2-256 (0x12), got %s (0x%x)\n",
566 res.Multihash.Name, res.Multihash.Code)
567 } else if res.Multihash.Length != 32 {
568 fmt.Fprintf(w, "CIDv0: not possible, requires 32-byte digest, got %d\n",
569 res.Multihash.Length)
570 }
571
572 fmt.Fprintf(w, "CIDv1: %s\n", res.CidV1)
573
574 return nil
575 }),
576 },
577 Type: CidInspectRes{},
578 Extra: CreateCmdExtras(SetDoesNotUseRepo(true)),
579 }
580
581 type multibaseSorter struct {
582 data []CodeAndName
583 }
584
585 func (s multibaseSorter) Sort() {
586 slices.SortFunc(s.data, func(a, b CodeAndName) int {
587 if n := cmp.Compare(unicode.ToLower(rune(a.Code)), unicode.ToLower(rune(b.Code))); n != 0 {
588 return n
589 }
590 // lowercase letters should come before uppercase
591 return cmp.Compare(b.Code, a.Code)
592 })
593 }
594
595 type codeAndNameSorter struct {
596 data []CodeAndName
597 }
598
599 func (s codeAndNameSorter) Sort() {
600 slices.SortFunc(s.data, func(a, b CodeAndName) int {
601 return cmp.Compare(a.Code, b.Code)
602 })
603 }