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 }