/ core / commands / cid.go
cid.go
  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  }