@cryptotaxi247 / kubo / commits / ae989328a

feat(cmd): add 'ipfs cid inspect' command (#11241)

* feat(cmd): add 'ipfs cid inspect' command Adds a new subcommand to inspect and display detailed CID information including version, multibase encoding, multicodec, and multihash components. Also shows equivalent CIDv0/CIDv1 representations. Example output: CID: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi Version: 1 Multibase: base32 (b) Multicodec: dag-pb (0x70) Multihash: sha2-256 (0x12) Length: 32 bytes Digest: c3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a CIDv0: QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR CIDv1: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi Supports --enc=json for machine-readable output. Fixes #11205 * refactor: tidy up CidInspectRes struct and add '/cid/inspect' route to tests This commit refines the CidInspectRes struct for better readability and consistency. Additionally, it includes the '/cid/inspect' route in the command tests to ensure comprehensive coverage of the new CID inspection functionality. * docs: update changelog for v0.42 to include new `ipfs cid inspect` command Added a section highlighting the new `ipfs cid inspect <cid>` command, detailing its functionality to display comprehensive CID information, including version, encoding, and hash details. The command supports machine-readable output and operates offline. * feat(cmd): improve ipfs cid inspect - multibase: always shown (implicit for CIDv0), prefix as string - multicodec/multihash: annotated (implicit) for CIDv0 - digest: uppercase hex with 0x prefix - cidV0: empty in JSON when not possible, text encoder explains why - cidV1: base36 for libp2p-key codec, base32 otherwise - errors: ErrorMsg kept for HTTP RPC API, text encoder returns non-zero exit - PeerID fallback: helpful hint with equivalent CID on invalid input - unknown codec/hash: graceful "unknown" label - stdin support via .EnableStdin() - inspect listed first in subcommands, cid format points to inspect - cli tests for all cases including JSON, PeerID, unknown codec * chore: move cid inspect changelog to v0.41 - digest: bare lowercase hex (no 0x prefix), matching sha256sum --------- Co-authored-by: Marcin Rataj <lidel@lidel.org>

jo lo committed Mar 20, 2026 at 06:53 UTC ae989328a178fad9fbf701de05f9a7eaa90a5f86
4 files changed +370 -5
core/commands/cid.go
+183 -5
@@ -2,6 +2,7 @@ package commands
2
3 import (
4 "cmp"
5 + "encoding/hex"
6 "errors"
7 "fmt"
8 "io"
@@ -14,6 +15,7 @@ import (
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"
@@ -24,11 +26,12 @@ var CidCmd = &cmds.Command{
26 Tagline: "Convert and discover properties of CIDs",
27 },
28 Subcommands: map[string]*cmds.Command{
27 - "format": cidFmtCmd,
28 - "base32": base32Cmd,
29 - "bases": basesCmd,
30 - "codecs": codecsCmd,
31 - "hashes": hashesCmd,
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 }
@@ -46,6 +49,8 @@ var cidFmtCmd = &cmds.Command{
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 },
@@ -400,6 +405,179 @@ var hashesCmd = &cmds.Command{
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 }
core/commands/commands_test.go
+1
@@ -40,6 +40,7 @@ func TestCommands(t *testing.T) {
40 "/cid/codecs",
41 "/cid/format",
42 "/cid/hashes",
43 + "/cid/inspect",
44 "/commands",
45 "/commands/completion",
46 "/commands/completion/bash",
docs/changelogs/v0.41.md
+20
@@ -11,6 +11,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
11 - [Overview](#overview)
12 - [🔦 Highlights](#-highlights)
13 - [🗑️ Faster Provide Queue Disk Reclamation](#-faster-provide-queue-disk-reclamation)
14 + - [✨ New `ipfs cid inspect` command](#-new-ipfs-cid-inspect-command)
15 - [🖥️ WebUI Improvements](#-webui-improvements)
16 - [🔧 Correct provider addresses for custom HTTP routing](#-correct-provider-addresses-for-custom-http-routing)
17 - [📦️ Dependency updates](#-dependency-updates)
@@ -42,6 +43,25 @@ To learn more, see [kubo#11096](https://github.com/ipfs/kubo/issues/11096),
43 [kubo#11198](https://github.com/ipfs/kubo/pull/11198), and
44 [go-libp2p-kad-dht#1233](https://github.com/libp2p/go-libp2p-kad-dht/pull/1233).
45
46 +#### ✨ New `ipfs cid inspect` command
47 +
48 +New subcommand for breaking down a CID into its components. Works offline, supports `--enc=json`.
49 +
50 +```console
51 +$ ipfs cid inspect bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi
52 +CID: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi
53 +Version: 1
54 +Multibase: base32 (b)
55 +Multicodec: dag-pb (0x70)
56 +Multihash: sha2-256 (0x12)
57 + Length: 32 bytes
58 + Digest: c3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a
59 +CIDv0: QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR
60 +CIDv1: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi
61 +```
62 +
63 +See `ipfs cid --help` for all CID-related commands.
64 +
65 #### 🖥️ WebUI Improvements
66
67 IPFS Web UI has been updated to [v4.12.0](https://github.com/ipfs/ipfs-webui/releases/tag/v4.12.0).
test/cli/cid_test.go
+166
@@ -1,17 +1,23 @@
1 package cli
2
3 import (
4 + "encoding/json"
5 "fmt"
6 "strings"
7 "testing"
8
9 + cid "github.com/ipfs/go-cid"
10 "github.com/ipfs/kubo/test/cli/harness"
11 + peer "github.com/libp2p/go-libp2p/core/peer"
12 + mhash "github.com/multiformats/go-multihash"
13 "github.com/stretchr/testify/assert"
14 + "github.com/stretchr/testify/require"
15 )
16
17 func TestCidCommands(t *testing.T) {
18 t.Parallel()
19
20 + t.Run("inspect", testCidInspect)
21 t.Run("base32", testCidBase32)
22 t.Run("format", testCidFormat)
23 t.Run("bases", testCidBases)
@@ -19,6 +25,166 @@ func TestCidCommands(t *testing.T) {
25 t.Run("hashes", testCidHashes)
26 }
27
28 +// testCidInspect tests 'ipfs cid inspect' subcommand
29 +func testCidInspect(t *testing.T) {
30 + t.Parallel()
31 + node := harness.NewT(t).NewNode()
32 +
33 + t.Run("CIDv0", func(t *testing.T) {
34 + res := node.RunIPFS("cid", "inspect", "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR")
35 + assert.Equal(t, 0, res.ExitCode())
36 + out := res.Stdout.String()
37 + assert.Contains(t, out, "CID: QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR")
38 + assert.Contains(t, out, "Version: 0")
39 + assert.Contains(t, out, "Multibase: base58btc (implicit)")
40 + assert.Contains(t, out, "Multicodec: dag-pb (0x70, implicit)")
41 + assert.Contains(t, out, "Multihash: sha2-256 (0x12, implicit)")
42 + assert.Contains(t, out, " Length: 32 bytes")
43 + assert.Contains(t, out, " Digest: c3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a")
44 + assert.Contains(t, out, "CIDv0: QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR")
45 + assert.Contains(t, out, "CIDv1: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
46 + })
47 +
48 + t.Run("CIDv1 base32 dag-pb", func(t *testing.T) {
49 + res := node.RunIPFS("cid", "inspect", "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
50 + assert.Equal(t, 0, res.ExitCode())
51 + out := res.Stdout.String()
52 + assert.Contains(t, out, "CID: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
53 + assert.Contains(t, out, "Version: 1")
54 + assert.Contains(t, out, "Multibase: base32 (b)")
55 + assert.Contains(t, out, "Multicodec: dag-pb (0x70)")
56 + assert.Contains(t, out, "Multihash: sha2-256 (0x12)")
57 + assert.Contains(t, out, " Length: 32 bytes")
58 + assert.Contains(t, out, " Digest: c3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a")
59 + assert.NotContains(t, out, "implicit")
60 + assert.Contains(t, out, "CIDv0: QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR")
61 + assert.Contains(t, out, "CIDv1: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
62 + })
63 +
64 + t.Run("CIDv1 raw codec", func(t *testing.T) {
65 + res := node.RunIPFS("cid", "inspect", "bafkreigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
66 + assert.Equal(t, 0, res.ExitCode())
67 + out := res.Stdout.String()
68 + assert.Contains(t, out, "CID: bafkreigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
69 + assert.Contains(t, out, "Multibase: base32 (b)")
70 + assert.Contains(t, out, "Multicodec: raw (0x55)")
71 + assert.Contains(t, out, "Multihash: sha2-256 (0x12)")
72 + assert.Contains(t, out, " Length: 32 bytes")
73 + assert.Contains(t, out, " Digest: c3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a")
74 + assert.Contains(t, out, "CIDv0: not possible, requires dag-pb (0x70), got raw (0x55)")
75 + assert.Contains(t, out, "CIDv1: bafkreigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
76 + })
77 +
78 + t.Run("CIDv1 base36", func(t *testing.T) {
79 + res := node.RunIPFS("cid", "inspect", "k2jmtxw8rjh1z69c6not3wtdxb0u3urbzhyll1t9jg6ox26dhi5sfi1m")
80 + assert.Equal(t, 0, res.ExitCode())
81 + out := res.Stdout.String()
82 + assert.Contains(t, out, "CID: k2jmtxw8rjh1z69c6not3wtdxb0u3urbzhyll1t9jg6ox26dhi5sfi1m")
83 + assert.Contains(t, out, "Multibase: base36 (k)")
84 + assert.Contains(t, out, "Multicodec: dag-pb (0x70)")
85 + assert.Contains(t, out, " Digest: c3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a")
86 + assert.Contains(t, out, "CIDv0: QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR")
87 + assert.Contains(t, out, "CIDv1: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
88 + })
89 +
90 + t.Run("invalid CID", func(t *testing.T) {
91 + res := node.RunIPFS("cid", "inspect", "garbage")
92 + assert.Equal(t, 1, res.ExitCode())
93 + assert.Contains(t, res.Stderr.String(), "invalid CID")
94 + })
95 +
96 + t.Run("PeerID as input", func(t *testing.T) {
97 + res := node.RunIPFS("cid", "inspect", "12D3KooWD3eckifWpRn9wQpMG9R9hX3sD158z7EqHWmweQAJU5SA")
98 + assert.Equal(t, 1, res.ExitCode())
99 + stderr := res.Stderr.String()
100 + assert.Contains(t, stderr, "PeerID")
101 + assert.Contains(t, stderr, "inspect its CID representation instead")
102 + // suggested CID should use base36 (k prefix)
103 + assert.Contains(t, stderr, "\n k")
104 + })
105 +
106 + t.Run("libp2p-key CID uses base36", func(t *testing.T) {
107 + // Construct a libp2p-key CIDv1 from a known PeerID
108 + pid, err := peer.Decode("12D3KooWD3eckifWpRn9wQpMG9R9hX3sD158z7EqHWmweQAJU5SA")
109 + require.NoError(t, err)
110 + pidCid := peer.ToCid(pid)
111 + cidStr := pidCid.String()
112 +
113 + res := node.RunIPFS("cid", "inspect", cidStr)
114 + assert.Equal(t, 0, res.ExitCode())
115 + out := res.Stdout.String()
116 + assert.Contains(t, out, "Multicodec: libp2p-key (0x72)")
117 + // CIDv1 should use base36 (k prefix)
118 + assert.Contains(t, out, "CIDv1: k")
119 + })
120 +
121 + t.Run("identity multihash CID", func(t *testing.T) {
122 + // raw codec + identity multihash: digest is the raw content ("test" = 74657374)
123 + res := node.RunIPFS("cid", "inspect", "bafkqabdumvzxi")
124 + assert.Equal(t, 0, res.ExitCode())
125 + out := res.Stdout.String()
126 + assert.Contains(t, out, "CID: bafkqabdumvzxi")
127 + assert.Contains(t, out, "Multicodec: raw (0x55)")
128 + assert.Contains(t, out, "Multihash: identity (0x0)")
129 + assert.Contains(t, out, " Length: 4 bytes")
130 + assert.Contains(t, out, " Digest: 74657374")
131 + })
132 +
133 + t.Run("unknown codec", func(t *testing.T) {
134 + // Construct a CID with unknown codec 0x9999
135 + mh, err := mhash.Sum([]byte("test"), mhash.SHA2_256, -1)
136 + require.NoError(t, err)
137 + unknownCID := cid.NewCidV1(0x9999, mh)
138 + cidStr := unknownCID.String()
139 +
140 + res := node.RunIPFS("cid", "inspect", cidStr)
141 + assert.Equal(t, 0, res.ExitCode())
142 + out := res.Stdout.String()
143 + assert.Contains(t, out, "Multicodec: unknown (0x9999)")
144 + assert.Contains(t, out, "not possible, requires dag-pb (0x70), got unknown (0x9999)")
145 + })
146 +
147 + t.Run("JSON output", func(t *testing.T) {
148 + res := node.RunIPFS("cid", "inspect", "--enc=json", "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
149 + assert.Equal(t, 0, res.ExitCode())
150 +
151 + var result map[string]any
152 + err := json.Unmarshal(res.Stdout.Bytes(), &result)
153 + require.NoError(t, err)
154 +
155 + // multibase.prefix should be a string, not a number
156 + mb := result["multibase"].(map[string]any)
157 + assert.IsType(t, "", mb["prefix"])
158 + assert.Equal(t, "b", mb["prefix"])
159 +
160 + // multihash.length should be a number (bytes)
161 + mh := result["multihash"].(map[string]any)
162 + assert.Equal(t, float64(32), mh["length"])
163 +
164 + // cidV0 should be a clean CID string, no explanatory text
165 + cidV0 := result["cidV0"].(string)
166 + assert.True(t, strings.HasPrefix(cidV0, "Qm"), "cidV0 should be a valid CIDv0")
167 +
168 + // cidV1 should be a clean CID string
169 + cidV1 := result["cidV1"].(string)
170 + assert.True(t, strings.HasPrefix(cidV1, "b"), "cidV1 should be base32 encoded")
171 + })
172 +
173 + t.Run("JSON output with empty CIDv0", func(t *testing.T) {
174 + // raw codec can't be CIDv0
175 + res := node.RunIPFS("cid", "inspect", "--enc=json", "bafkreigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
176 + assert.Equal(t, 0, res.ExitCode())
177 +
178 + var result map[string]any
179 + err := json.Unmarshal(res.Stdout.Bytes(), &result)
180 + require.NoError(t, err)
181 +
182 + // cidV0 should not be present (omitempty)
183 + _, hasCidV0 := result["cidV0"]
184 + assert.False(t, hasCidV0, "cidV0 should be omitted when not possible")
185 + })
186 +}
187 +
188 // testCidBase32 tests 'ipfs cid base32' subcommand
189 // Includes regression tests for https://github.com/ipfs/kubo/issues/9007
190 func testCidBase32(t *testing.T) {