@cryptotaxi247 / kubo / commits / 237251a00

fix(cli/rpc): --cid-base works in all commands (#11239)

* fix: --cid-base works in all commands and auto-upgrades CIDv0 Passing --cid-base=base32 now returns CIDv1 in base32 everywhere, including block, dag stat, and object patch which previously ignored it. - cidbase: auto-upgrade CIDv0 when base is not base58btc, deprecate --upgrade-cidv0-in-output, remove GetLowLevelCidEncoder - block stat/put/rm: use GetCidEncoder - dag stat: store CID as pre-encoded string, drop MarshalJSON/UnmarshalJSON - object patch rm-link/add-link: use GetCidEncoder - bitswap: switch to GetCidEncoder * test: add harness tests for --cid-base flag Remove unused DagStat.String() which truncated CIDs. Add CLI tests for --cid-base across block, dag stat, and object patch commands, including the --format=v0 interaction. * fix: respect --cid-base in refs local, object diff, pin remote, files chroot Use GetCidEncoder in commands that were still outputting CIDs via raw .String() calls. - refs local: encode blockstore keys with the requested base - object diff: encode Before/After CIDs in text encoder - pin remote add/ls: pass encoder through toRemotePinOutput - files chroot: encode old/new root CIDs in status message - tests: use base16 to avoid false positives if base32 becomes default * docs: update changelog entry for --cid-base fixes * test: cover --cid-base for add, pin ls, dag import Add harness tests for add, add -Q, pin ls, and dag import. Fix object patch tests broken by upstream UnixFS validation. Use base16 in all tests to avoid false positives. * docs: add metrics and CARv2 highlights to v0.41 changelog

Marcin Rataj committed Apr 11, 2026 at 01:43 UTC 237251a0068e7f6c5cddcde7992143c5a15621a0
13 files changed +415 -93
core/commands/bitswap.go
+2 -2
@@ -80,7 +80,7 @@ Print out all blocks currently on the bitswap wantlist for the local peer.`,
80 },
81 Encoders: cmds.EncoderMap{
82 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *KeyList) error {
83 - enc, err := cmdenv.GetLowLevelCidEncoder(req)
83 + enc, err := cmdenv.GetCidEncoder(req)
84 if err != nil {
85 return err
86 }
@@ -128,7 +128,7 @@ var bitswapStatCmd = &cmds.Command{
128 },
129 Encoders: cmds.EncoderMap{
130 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, s *bitswap.Stat) error {
131 - enc, err := cmdenv.GetLowLevelCidEncoder(req)
131 + enc, err := cmdenv.GetCidEncoder(req)
132 if err != nil {
133 return err
134 }
core/commands/block.go
+19 -4
@@ -67,6 +67,11 @@ on raw IPFS blocks. It outputs the following to stdout:
67 return err
68 }
69
70 + enc, err := cmdenv.GetCidEncoder(req)
71 + if err != nil {
72 + return err
73 + }
74 +
75 p, err := cmdutils.PathOrCidPath(req.Arguments[0])
76 if err != nil {
77 return err
@@ -78,7 +83,7 @@ on raw IPFS blocks. It outputs the following to stdout:
83 }
84
85 return cmds.EmitOnce(res, &BlockStat{
81 - Key: b.Path().RootCid().String(),
86 + Key: enc.Encode(b.Path().RootCid()),
87 Size: b.Size(),
88 })
89 },
@@ -171,6 +176,11 @@ only for backward compatibility when a legacy CIDv0 is required (--format=v0).
176 return err
177 }
178
179 + enc, err := cmdenv.GetCidEncoder(req)
180 + if err != nil {
181 + return err
182 + }
183 +
184 nd, err := cmdenv.GetNode(env)
185 if err != nil {
186 return err
@@ -230,7 +240,7 @@ only for backward compatibility when a legacy CIDv0 is required (--format=v0).
240 }
241
242 err = res.Emit(&BlockStat{
233 - Key: p.Path().RootCid().String(),
243 + Key: enc.Encode(p.Path().RootCid()),
244 Size: p.Size(),
245 })
246 if err != nil {
@@ -280,6 +290,11 @@ It takes a list of CIDs to remove from the local datastore..
290 return err
291 }
292
293 + enc, err := cmdenv.GetCidEncoder(req)
294 + if err != nil {
295 + return err
296 + }
297 +
298 force, _ := req.Options[forceOptionName].(bool)
299 quiet, _ := req.Options[blockQuietOptionName].(bool)
300
@@ -298,7 +313,7 @@ It takes a list of CIDs to remove from the local datastore..
313 err = api.Block().Rm(req.Context, rp, options.Block.Force(force))
314 if err != nil {
315 if err := res.Emit(&removedBlock{
301 - Hash: rp.RootCid().String(),
316 + Hash: enc.Encode(rp.RootCid()),
317 Error: err.Error(),
318 }); err != nil {
319 return err
@@ -308,7 +323,7 @@ It takes a list of CIDs to remove from the local datastore..
323
324 if !quiet {
325 err := res.Emit(&removedBlock{
311 - Hash: rp.RootCid().String(),
326 + Hash: enc.Encode(rp.RootCid()),
327 })
328 if err != nil {
329 return err
core/commands/cmdenv/cidbase.go
+17 -16
@@ -11,24 +11,20 @@ import (
11 )
12
13 var (
14 - OptionCidBase = cmds.StringOption("cid-base", "Multibase encoding used for version 1 CIDs in output.")
15 - OptionUpgradeCidV0InOutput = cmds.BoolOption("upgrade-cidv0-in-output", "Upgrade version 0 to version 1 CIDs in output.")
14 + OptionCidBase = cmds.StringOption("cid-base", "Multibase encoding for CIDs in output. CIDv0 is automatically converted to CIDv1 when a base other than base58btc is specified.")
15 +
16 + // OptionUpgradeCidV0InOutput is deprecated. When --cid-base is set to
17 + // anything other than base58btc, CIDv0 are now automatically upgraded
18 + // to CIDv1. This flag is kept for backward compatibility and will be
19 + // removed in a future release.
20 + OptionUpgradeCidV0InOutput = cmds.BoolOption("upgrade-cidv0-in-output", "[DEPRECATED] Upgrade version 0 to version 1 CIDs in output.")
21 )
22
18 -// GetCidEncoder processes the `cid-base` and `output-cidv1` options and
19 -// returns an encoder to use based on those parameters.
23 +// GetCidEncoder processes the --cid-base option and returns an encoder.
24 +// When --cid-base is set to a non-base58btc encoding, CIDv0 values are
25 +// automatically upgraded to CIDv1 because CIDv0 can only be represented
26 +// in base58btc.
27 func GetCidEncoder(req *cmds.Request) (cidenc.Encoder, error) {
21 - return getCidBase(req, true)
22 -}
23 -
24 -// GetLowLevelCidEncoder is like GetCidEncoder but meant to be used by lower
25 -// level commands. It differs from GetCidEncoder in that CIDv0 are not, by
26 -// default, auto-upgraded to CIDv1.
27 -func GetLowLevelCidEncoder(req *cmds.Request) (cidenc.Encoder, error) {
28 - return getCidBase(req, false)
29 -}
30 -
31 -func getCidBase(req *cmds.Request, autoUpgrade bool) (cidenc.Encoder, error) {
28 base, _ := req.Options[OptionCidBase.Name()].(string)
29 upgrade, upgradeDefined := req.Options[OptionUpgradeCidV0InOutput.Name()].(bool)
30
@@ -40,11 +36,16 @@ func getCidBase(req *cmds.Request, autoUpgrade bool) (cidenc.Encoder, error) {
36 if err != nil {
37 return e, err
38 }
43 - if autoUpgrade {
39 + // CIDv0 can only be represented in base58btc. When any other
40 + // base is requested, always upgrade CIDv0 to CIDv1 so the
41 + // output actually uses the requested encoding.
42 + if e.Base.Encoding() != mbase.Base58BTC {
43 e.Upgrade = true
44 }
45 }
46
47 + // Deprecated: --upgrade-cidv0-in-output still works as an explicit
48 + // override for backward compatibility.
49 if upgradeDefined {
50 e.Upgrade = upgrade
51 }
core/commands/cmdenv/cidbase_test.go
+73
@@ -4,9 +4,82 @@ import (
4 "testing"
5
6 cidenc "github.com/ipfs/go-cidutil/cidenc"
7 + cmds "github.com/ipfs/go-ipfs-cmds"
8 mbase "github.com/multiformats/go-multibase"
9 )
10
11 +func TestGetCidEncoder(t *testing.T) {
12 + makeReq := func(opts map[string]any) *cmds.Request {
13 + if opts == nil {
14 + opts = map[string]any{}
15 + }
16 + return &cmds.Request{Options: opts}
17 + }
18 +
19 + t.Run("no options returns default encoder", func(t *testing.T) {
20 + enc, err := GetCidEncoder(makeReq(nil))
21 + if err != nil {
22 + t.Fatal(err)
23 + }
24 + if enc.Upgrade {
25 + t.Error("expected Upgrade=false with no options")
26 + }
27 + })
28 +
29 + t.Run("non-base58btc base auto-upgrades CIDv0", func(t *testing.T) {
30 + enc, err := GetCidEncoder(makeReq(map[string]any{
31 + "cid-base": "base32",
32 + }))
33 + if err != nil {
34 + t.Fatal(err)
35 + }
36 + if !enc.Upgrade {
37 + t.Error("expected Upgrade=true for base32")
38 + }
39 + if enc.Base.Encoding() != mbase.Base32 {
40 + t.Errorf("expected base32 encoding, got %v", enc.Base.Encoding())
41 + }
42 + })
43 +
44 + t.Run("base58btc does not auto-upgrade", func(t *testing.T) {
45 + enc, err := GetCidEncoder(makeReq(map[string]any{
46 + "cid-base": "base58btc",
47 + }))
48 + if err != nil {
49 + t.Fatal(err)
50 + }
51 + if enc.Upgrade {
52 + t.Error("expected Upgrade=false for base58btc")
53 + }
54 + })
55 +
56 + t.Run("deprecated flag still works as override", func(t *testing.T) {
57 + // Explicitly disable upgrade even with non-base58btc base
58 + enc, err := GetCidEncoder(makeReq(map[string]any{
59 + "cid-base": "base32",
60 + "upgrade-cidv0-in-output": false,
61 + }))
62 + if err != nil {
63 + t.Fatal(err)
64 + }
65 + if enc.Upgrade {
66 + t.Error("expected Upgrade=false when explicitly disabled")
67 + }
68 +
69 + // Explicitly enable upgrade even with base58btc
70 + enc, err = GetCidEncoder(makeReq(map[string]any{
71 + "cid-base": "base58btc",
72 + "upgrade-cidv0-in-output": true,
73 + }))
74 + if err != nil {
75 + t.Fatal(err)
76 + }
77 + if !enc.Upgrade {
78 + t.Error("expected Upgrade=true when explicitly enabled")
79 + }
80 + })
81 +}
82 +
83 func TestEncoderFromPath(t *testing.T) {
84 test := func(path string, expected cidenc.Encoder) {
85 actual, err := CidEncoderFromPath(path)
core/commands/dag/dag.go
+10 -53
@@ -98,7 +98,7 @@ into an object of the specified format.
98 Type: OutputObject{},
99 Encoders: cmds.EncoderMap{
100 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *OutputObject) error {
101 - enc, err := cmdenv.GetLowLevelCidEncoder(req)
101 + enc, err := cmdenv.GetCidEncoder(req)
102 if err != nil {
103 return err
104 }
@@ -154,7 +154,7 @@ var DagResolveCmd = &cmds.Command{
154 // Nope, fallback on the default.
155 fallthrough
156 default:
157 - enc, err = cmdenv.GetLowLevelCidEncoder(req)
157 + enc, err = cmdenv.GetCidEncoder(req)
158 if err != nil {
159 return err
160 }
@@ -246,7 +246,7 @@ Specification of CAR formats: https://ipld.io/specs/transport/car/
246 return fmt.Errorf("unexpected message from DAG import")
247 }
248
249 - enc, err := cmdenv.GetLowLevelCidEncoder(req)
249 + enc, err := cmdenv.GetCidEncoder(req)
250 if err != nil {
251 return err
252 }
@@ -294,57 +294,15 @@ CAR file follows the CARv1 format: https://ipld.io/specs/transport/car/carv1/
294 },
295 }
296
297 -// DagStat is a dag stat command response
297 +// DagStat is a dag stat command response. Cid is stored as a
298 +// pre-encoded string (via GetCidEncoder in the Run handler) so that
299 +// --cid-base is respected and no custom MarshalJSON is needed.
300 type DagStat struct {
299 - Cid cid.Cid
301 + Cid string `json:"Cid"`
302 Size uint64 `json:",omitempty"`
303 NumBlocks int64 `json:",omitempty"`
304 }
305
304 -func (s *DagStat) String() string {
305 - return fmt.Sprintf("%s %d %d", s.Cid.String()[:20], s.Size, s.NumBlocks)
306 -}
307 -
308 -func (s *DagStat) MarshalJSON() ([]byte, error) {
309 - type Alias DagStat
310 - /*
311 - We can't rely on cid.Cid.MarshalJSON since it uses the {"/": "..."}
312 - format. To make the output consistent and follow the Kubo API patterns
313 - we use the Cid.String method
314 - */
315 - return json.Marshal(struct {
316 - Cid string `json:"Cid"`
317 - *Alias
318 - }{
319 - Cid: s.Cid.String(),
320 - Alias: (*Alias)(s),
321 - })
322 -}
323 -
324 -func (s *DagStat) UnmarshalJSON(data []byte) error {
325 - /*
326 - We can't rely on cid.Cid.UnmarshalJSON since it uses the {"/": "..."}
327 - format. To make the output consistent and follow the Kubo API patterns
328 - we use the Cid.Parse method
329 - */
330 - type Alias DagStat
331 - aux := struct {
332 - Cid string `json:"Cid"`
333 - *Alias
334 - }{
335 - Alias: (*Alias)(s),
336 - }
337 - if err := json.Unmarshal(data, &aux); err != nil {
338 - return err
339 - }
340 - Cid, err := cid.Parse(aux.Cid)
341 - if err != nil {
342 - return err
343 - }
344 - s.Cid = Cid
345 - return nil
346 -}
347 -
306 type DagStatSummary struct {
307 redundantSize uint64 `json:"-"`
308 UniqueBlocks int `json:",omitempty"`
@@ -406,7 +364,7 @@ Note: This command skips duplicate blocks in reporting both size and the number
364 fmt.Fprintln(w)
365 csvWriter := csv.NewWriter(w)
366 csvWriter.Comma = '\t'
409 - cidSpacing := len(event.DagStatsArray[0].Cid.String())
367 + cidSpacing := len(event.DagStatsArray[0].Cid)
368 header := []string{fmt.Sprintf("%-*s", cidSpacing, "CID"), fmt.Sprintf("%-15s", "Blocks"), "Size"}
369 if err := csvWriter.Write(header); err != nil {
370 return err
@@ -414,7 +372,7 @@ Note: This command skips duplicate blocks in reporting both size and the number
372 for _, dagStat := range event.DagStatsArray {
373 numBlocksStr := fmt.Sprint(dagStat.NumBlocks)
374 err := csvWriter.Write([]string{
417 - dagStat.Cid.String(),
375 + dagStat.Cid,
376 fmt.Sprintf("%-15s", numBlocksStr),
377 fmt.Sprint(dagStat.Size),
378 })
@@ -434,7 +392,6 @@ Note: This command skips duplicate blocks in reporting both size and the number
392 }),
393 cmds.JSON: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, event *DagStatSummary) error {
394 return json.NewEncoder(w).Encode(event)
437 - },
438 - ),
395 + }),
396 },
397 }
core/commands/dag/stat.go
+7 -1
@@ -29,6 +29,12 @@ func dagStat(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment)
29 if err != nil {
30 return err
31 }
32 +
33 + enc, err := cmdenv.GetCidEncoder(req)
34 + if err != nil {
35 + return err
36 + }
37 +
38 nodeGetter := mdag.NewSession(req.Context, api.Dag())
39
40 cidSet := cid.NewSet()
@@ -50,7 +56,7 @@ func dagStat(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment)
56 if err != nil {
57 return err
58 }
53 - dagstats := &DagStat{Cid: rp.RootCid()}
59 + dagstats := &DagStat{Cid: enc.Encode(rp.RootCid())}
60 dagStatSummary.appendStats(dagstats)
61 err = traverse.Traverse(obj, traverse.Options{
62 DAG: nodeGetter,
core/commands/files.go
+10 -4
@@ -1706,6 +1706,11 @@ Examples:
1706 return errors.New("this is a potentially destructive operation; pass --confirm to proceed")
1707 }
1708
1709 + enc, err := cmdenv.GetCidEncoder(req)
1710 + if err != nil {
1711 + return err
1712 + }
1713 +
1714 // Determine new root CID
1715 var newRootCid cid.Cid
1716 if len(req.Arguments) > 0 {
@@ -1742,7 +1747,7 @@ Examples:
1747 // Special case: empty dir is always available (hardcoded in boxo)
1748 emptyDirCid := ft.EmptyDirNode().Cid()
1749 if !newRootCid.Equals(emptyDirCid) {
1745 - return fmt.Errorf("new root %s does not exist locally; fetch it first with 'ipfs block get'", newRootCid)
1750 + return fmt.Errorf("new root %s does not exist locally; fetch it first with 'ipfs block get'", enc.Encode(newRootCid))
1751 }
1752 }
1753
@@ -1771,7 +1776,7 @@ Examples:
1776 if err == nil {
1777 oldRootCid, err := cid.Cast(oldRootBytes)
1778 if err == nil {
1774 - oldRootStr = oldRootCid.String()
1779 + oldRootStr = enc.Encode(oldRootCid)
1780 }
1781 } else if !errors.Is(err, datastore.ErrNotFound) {
1782 return fmt.Errorf("reading current MFS root: %w", err)
@@ -1784,12 +1789,13 @@ Examples:
1789 }
1790
1791 // Build output message
1792 + newRootStr := enc.Encode(newRootCid)
1793 var msg string
1794 if oldRootStr != "" {
1789 - msg = fmt.Sprintf("MFS root changed from %s to %s\n", oldRootStr, newRootCid)
1795 + msg = fmt.Sprintf("MFS root changed from %s to %s\n", oldRootStr, newRootStr)
1796 msg += fmt.Sprintf("The old root %s will be garbage collected unless pinned.\n", oldRootStr)
1797 } else {
1792 - msg = fmt.Sprintf("MFS root set to %s\n", newRootCid)
1798 + msg = fmt.Sprintf("MFS root set to %s\n", newRootStr)
1799 }
1800
1801 return cmds.EmitOnce(res, &MessageOutput{Message: msg})
core/commands/object/diff.go
+10 -6
@@ -97,26 +97,30 @@ Example:
97 Type: Changes{},
98 Encoders: cmds.EncoderMap{
99 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *Changes) error {
100 + enc, err := cmdenv.GetCidEncoder(req)
101 + if err != nil {
102 + return err
103 + }
104 verbose, _ := req.Options[verboseOptionName].(bool)
105
106 for _, change := range out.Changes {
107 if verbose {
108 switch change.Type {
109 case dagutils.Add:
106 - fmt.Fprintf(w, "Added new link %q pointing to %s.\n", change.Path, change.After)
110 + fmt.Fprintf(w, "Added new link %q pointing to %s.\n", change.Path, enc.Encode(change.After))
111 case dagutils.Mod:
108 - fmt.Fprintf(w, "Changed %q from %s to %s.\n", change.Path, change.Before, change.After)
112 + fmt.Fprintf(w, "Changed %q from %s to %s.\n", change.Path, enc.Encode(change.Before), enc.Encode(change.After))
113 case dagutils.Remove:
110 - fmt.Fprintf(w, "Removed link %q (was %s).\n", change.Path, change.Before)
114 + fmt.Fprintf(w, "Removed link %q (was %s).\n", change.Path, enc.Encode(change.Before))
115 }
116 } else {
117 switch change.Type {
118 case dagutils.Add:
115 - fmt.Fprintf(w, "+ %s %q\n", change.After, change.Path)
119 + fmt.Fprintf(w, "+ %s %q\n", enc.Encode(change.After), change.Path)
120 case dagutils.Mod:
117 - fmt.Fprintf(w, "~ %s %s %q\n", change.Before, change.After, change.Path)
121 + fmt.Fprintf(w, "~ %s %s %q\n", enc.Encode(change.Before), enc.Encode(change.After), change.Path)
122 case dagutils.Remove:
119 - fmt.Fprintf(w, "- %s %q\n", change.Before, change.Path)
123 + fmt.Fprintf(w, "- %s %q\n", enc.Encode(change.Before), change.Path)
124 }
125 }
126 }
core/commands/object/patch.go
+12 -2
@@ -77,6 +77,11 @@ use 'ipfs files rm' instead: 'ipfs files --help'.
77 return err
78 }
79
80 + enc, err := cmdenv.GetCidEncoder(req)
81 + if err != nil {
82 + return err
83 + }
84 +
85 root, err := cmdutils.PathOrCidPath(req.Arguments[0])
86 if err != nil {
87 return err
@@ -94,7 +99,7 @@ use 'ipfs files rm' instead: 'ipfs files --help'.
99 return err
100 }
101
97 - return cmds.EmitOnce(res, &Object{Hash: p.RootCid().String()})
102 + return cmds.EmitOnce(res, &Object{Hash: enc.Encode(p.RootCid())})
103 },
104 Type: Object{},
105 Encoders: cmds.EncoderMap{
@@ -153,6 +158,11 @@ use MFS and 'files' commands instead: 'ipfs files --help'.
158 return err
159 }
160
161 + enc, err := cmdenv.GetCidEncoder(req)
162 + if err != nil {
163 + return err
164 + }
165 +
166 root, err := cmdutils.PathOrCidPath(req.Arguments[0])
167 if err != nil {
168 return err
@@ -182,7 +192,7 @@ use MFS and 'files' commands instead: 'ipfs files --help'.
192 return err
193 }
194
185 - return cmds.EmitOnce(res, &Object{Hash: p.RootCid().String()})
195 + return cmds.EmitOnce(res, &Object{Hash: enc.Encode(p.RootCid())})
196 },
197 Type: Object{},
198 Encoders: cmds.EncoderMap{
core/commands/pin/remotepin.go
+15 -4
@@ -17,6 +17,7 @@ import (
17
18 pinclient "github.com/ipfs/boxo/pinning/remote/client"
19 cid "github.com/ipfs/go-cid"
20 + cidenc "github.com/ipfs/go-cidutil/cidenc"
21 cmds "github.com/ipfs/go-ipfs-cmds"
22 logging "github.com/ipfs/go-log/v2"
23 config "github.com/ipfs/kubo/config"
@@ -73,11 +74,11 @@ type RemotePinOutput struct {
74 Name string
75 }
76
76 -func toRemotePinOutput(ps pinclient.PinStatusGetter) RemotePinOutput {
77 +func toRemotePinOutput(ps pinclient.PinStatusGetter, enc cidenc.Encoder) RemotePinOutput {
78 return RemotePinOutput{
79 Name: ps.GetPin().GetName(),
80 Status: ps.GetStatus().String(),
80 - Cid: ps.GetPin().GetCid().String(),
81 + Cid: enc.Encode(ps.GetPin().GetCid()),
82 }
83 }
84
@@ -143,6 +144,11 @@ NOTE: a comma-separated notation is supported in CLI for convenience:
144 ctx, cancel := context.WithCancel(req.Context)
145 defer cancel()
146
147 + enc, err := cmdenv.GetCidEncoder(req)
148 + if err != nil {
149 + return err
150 + }
151 +
152 // Get remote service
153 c, err := getRemotePinServiceFromRequest(req, env)
154 if err != nil {
@@ -257,7 +263,7 @@ NOTE: a comma-separated notation is supported in CLI for convenience:
263 }
264 }
265
260 - return res.Emit(toRemotePinOutput(ps))
266 + return res.Emit(toRemotePinOutput(ps, enc))
267 },
268 Encoders: cmds.EncoderMap{
269 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *RemotePinOutput) error {
@@ -294,6 +300,11 @@ Pass '--status=queued,pinning,pinned,failed' to list pins in all states.
300 return err
301 }
302
303 + enc, err := cmdenv.GetCidEncoder(req)
304 + if err != nil {
305 + return err
306 + }
307 +
308 ctx, cancel := context.WithCancel(req.Context)
309 defer cancel()
310
@@ -303,7 +314,7 @@ Pass '--status=queued,pinning,pinned,failed' to list pins in all states.
314 lsErr <- lsRemote(ctx, req, c, psCh)
315 }()
316 for ps := range psCh {
306 - if err := res.Emit(toRemotePinOutput(ps)); err != nil {
317 + if err := res.Emit(toRemotePinOutput(ps, enc)); err != nil {
318 return err
319 }
320 }
core/commands/refs.go
+6 -1
@@ -149,6 +149,11 @@ Displays the hashes of all local objects. NOTE: This treats all local objects as
149 return err
150 }
151
152 + enc, err := cmdenv.GetCidEncoder(req)
153 + if err != nil {
154 + return err
155 + }
156 +
157 // todo: make async
158 allKeys, err := n.Blockstore.AllKeysChan(ctx)
159 if err != nil {
@@ -156,7 +161,7 @@ Displays the hashes of all local objects. NOTE: This treats all local objects as
161 }
162
163 for k := range allKeys {
159 - err := res.Emit(&RefWrapper{Ref: k.String()})
164 + err := res.Emit(&RefWrapper{Ref: enc.Encode(k)})
165 if err != nil {
166 return err
167 }
docs/changelogs/v0.41.md
+17
@@ -12,6 +12,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
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 + - [🔤 `--cid-base` fixes across all commands](#-cid-base-fixes-across-all-commands)
16 - [🔄 Built-in `ipfs update` command](#-built-in-ipfs-update-command)
17 - [🖥️ WebUI Improvements](#-webui-improvements)
18 - [🔧 Correct provider addresses for custom HTTP routing](#-correct-provider-addresses-for-custom-http-routing)
@@ -23,6 +24,8 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
24 - [🛡️ `ipfs object patch` validates UnixFS node types](#-ipfs-object-patch-validates-unixfs-node-types)
25 - [🔗 MFS: fixed CidBuilder preservation](#-mfs-fixed-cidbuilder-preservation)
26 - [📂 FUSE Mount Improvements](#-fuse-mount-improvements)
27 + - [📊 Dropped high-cardinality `server.address` from HTTP metrics](#-dropped-high-cardinality-serveraddress-from-http-metrics)
28 + - [📦 CARv2 import over HTTP API](#-carv2-import-over-http-api)
29 - [🐹 Go 1.26, Once More with Feeling](#-go-126-once-more-with-feeling)
30 - [📦️ Dependency updates](#-dependency-updates)
31 - [📝 Changelog](#-changelog)
@@ -72,6 +75,12 @@ CIDv1: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi
75
76 See `ipfs cid --help` for all CID-related commands.
77
78 +#### 🔤 `--cid-base` fixes across all commands
79 +
80 +`--cid-base` is now respected by every command that outputs CIDs. Previously `block stat`, `block put`, `block rm`, `dag stat`, `refs local`, `pin remote`, and `files chroot` ignored the flag.
81 +
82 +CIDv0 values are now auto-upgraded to CIDv1 when a non-base58btc base is requested, because CIDv0 can only be represented in base58btc.
83 +
84 #### 🔄 Built-in `ipfs update` command
85
86 Kubo now ships with a built-in `ipfs update` command that downloads release binaries from GitHub and swaps the current one in place. It supersedes the external [`ipfs-update`](https://github.com/ipfs/ipfs-update) tool, deprecated since [v0.37](https://github.com/ipfs/kubo/blob/master/docs/changelogs/v0.37.md#-repository-migration-from-v16-to-v17-with-embedded-tooling).
@@ -194,6 +203,14 @@ The FUSE implementation has been rewritten on top of [`hanwen/go-fuse` v2](https
203 - **`statfs` works.** All three mounts report the free space of the volume backing the local IPFS repo, so `/mfs` correctly reflects how much new data can be onboarded. Fixes macOS Finder refusing copies with "not enough free space".
204 - **Platform compatibility.** macOS detection updated from OSXFUSE 2.x to macFUSE 4.x. Linux no longer needs a `fusermount` symlink; [`hanwen/go-fuse`](https://github.com/hanwen/go-fuse) finds `fusermount3` natively.
205
206 +#### 📊 Dropped high-cardinality `server.address` from HTTP metrics
207 +
208 +The `server.address` attribute (derived from the `Host` header) has been removed from `http.server.*` metrics. On subdomain gateways every CID produced a unique time series, causing multi-gigabyte Prometheus responses. A new `server.domain` attribute groups requests by the gateway's public suffix (e.g. `dweb.link`) instead. See [#11208](https://github.com/ipfs/kubo/pull/11208) and [docs/metrics.md](https://github.com/ipfs/kubo/blob/master/docs/metrics.md).
209 +
210 +#### 📦 CARv2 import over HTTP API
211 +
212 +`ipfs dag import` of CARv2 files now works over the HTTP API. Previously it failed with `operation not supported` because the HTTP multipart stream falsely advertised seek support, which go-car relied on for CARv2 payload offset. See [#11253](https://github.com/ipfs/kubo/pull/11253).
213 +
214 #### 🐹 Go 1.26, Once More with Feeling
215
216 Kubo first shipped with [Go 1.26](https://go.dev/doc/go1.26) in v0.40.0, but [v0.40.1](https://github.com/ipfs/kubo/blob/master/docs/changelogs/v0.40.md#v0401) had to downgrade to Go 1.25 because of a Windows crash in Go's overlapped I/O layer ([#11214](https://github.com/ipfs/kubo/issues/11214)). Go 1.26.2 fixes that regression upstream ([golang/go#78041](https://github.com/golang/go/issues/78041)), so Kubo is back on Go 1.26 across all platforms.
test/cli/cid_base_test.go new
+217
@@ -0,0 +1,217 @@
1 +package cli
2 +
3 +import (
4 + "bytes"
5 + "encoding/json"
6 + "strings"
7 + "testing"
8 +
9 + "github.com/ipfs/kubo/test/cli/harness"
10 + "github.com/stretchr/testify/require"
11 +)
12 +
13 +// TestCidBase verifies that --cid-base is respected across commands
14 +// and that CIDv0 is auto-upgraded to CIDv1 when a non-base58btc base
15 +// is requested.
16 +//
17 +// Tests use base16 rather than base32 to avoid false positives if
18 +// base32 ever becomes the default CID encoding.
19 +func TestCidBase(t *testing.T) {
20 + t.Parallel()
21 +
22 + const cidBaseFlag = "--cid-base=base16"
23 + // base16 CIDv1 starts with "f01" (f = base16 multibase prefix)
24 + const cidV1Prefix = "f01"
25 +
26 + makeDaemon := func(t *testing.T) *harness.Node {
27 + t.Helper()
28 + node := harness.NewT(t).NewNode().Init().StartDaemon("--offline")
29 + t.Cleanup(func() { node.StopDaemon() })
30 + return node
31 + }
32 +
33 + t.Run("add respects --cid-base", func(t *testing.T) {
34 + t.Parallel()
35 + node := makeDaemon(t)
36 +
37 + // ipfs add -q
38 + cid := node.IPFSAddStr("test-add", cidBaseFlag)
39 + require.True(t, strings.HasPrefix(cid, cidV1Prefix), "expected base16 CIDv1 from add, got %s", cid)
40 +
41 + // ipfs add -Q (quiet, only final CID)
42 + cid = node.PipeStrToIPFS("test-add-Q", "add", "-Q", cidBaseFlag).Stdout.Trimmed()
43 + require.True(t, strings.HasPrefix(cid, cidV1Prefix), "expected base16 CIDv1 from add -Q, got %s", cid)
44 + })
45 +
46 + t.Run("pin ls respects --cid-base", func(t *testing.T) {
47 + t.Parallel()
48 + node := makeDaemon(t)
49 +
50 + node.IPFSAddStr("pin-ls-test")
51 +
52 + lines := node.IPFS("pin", "ls", "-t", "recursive", cidBaseFlag).Stdout.Lines()
53 + for _, line := range lines {
54 + if line == "" {
55 + continue
56 + }
57 + require.True(t, strings.HasPrefix(line, cidV1Prefix), "expected base16 CID in pin ls, got %s", line)
58 + }
59 + })
60 +
61 + t.Run("dag import respects --cid-base", func(t *testing.T) {
62 + t.Parallel()
63 + node := makeDaemon(t)
64 +
65 + // Add content and export as CAR
66 + cid := node.IPFSAddStr("dag-import-test", "--pin=false")
67 + carData := node.IPFS("dag", "export", cid).Stdout.Bytes()
68 +
69 + // Import the CAR with --cid-base
70 + out := node.PipeToIPFS(bytes.NewReader(carData), "dag", "import", cidBaseFlag).Stdout.Trimmed()
71 + require.Contains(t, out, cidV1Prefix, "expected base16 CID in dag import output, got %s", out)
72 + })
73 +
74 + t.Run("block put returns base16 CIDv1", func(t *testing.T) {
75 + t.Parallel()
76 + node := makeDaemon(t)
77 + cid := node.PipeStrToIPFS("hello", "block", "put", cidBaseFlag).Stdout.Trimmed()
78 + require.True(t, strings.HasPrefix(cid, cidV1Prefix), "expected base16 CIDv1, got %s", cid)
79 + })
80 +
81 + t.Run("block put --format=v0 auto-upgrades to CIDv1 with --cid-base", func(t *testing.T) {
82 + t.Parallel()
83 + node := makeDaemon(t)
84 +
85 + // Without --cid-base: CIDv0 in base58btc
86 + cidV0 := node.PipeStrToIPFS("hello", "block", "put", "--format=v0").Stdout.Trimmed()
87 + require.True(t, strings.HasPrefix(cidV0, "Qm"), "expected CIDv0, got %s", cidV0)
88 +
89 + // With --cid-base: same content but displayed as CIDv1
90 + cidV1 := node.PipeStrToIPFS("hello", "block", "put", "--format=v0", cidBaseFlag).Stdout.Trimmed()
91 + require.True(t, strings.HasPrefix(cidV1, cidV1Prefix), "expected base16 CIDv1, got %s", cidV1)
92 + })
93 +
94 + t.Run("block stat respects --cid-base", func(t *testing.T) {
95 + t.Parallel()
96 + node := makeDaemon(t)
97 +
98 + cidV0 := node.PipeStrToIPFS("test-block-stat", "block", "put", "--format=v0").Stdout.Trimmed()
99 + require.True(t, strings.HasPrefix(cidV0, "Qm"))
100 +
101 + // block stat without --cid-base returns CIDv0
102 + stat := node.IPFS("block", "stat", cidV0).Stdout.Trimmed()
103 + require.Contains(t, stat, cidV0)
104 +
105 + // block stat with --cid-base returns CIDv1
106 + stat = node.IPFS("block", "stat", cidBaseFlag, cidV0).Stdout.Trimmed()
107 + require.NotContains(t, stat, cidV0, "should not contain CIDv0")
108 + require.Contains(t, stat, cidV1Prefix, "should contain base16 CIDv1")
109 + })
110 +
111 + t.Run("block rm respects --cid-base", func(t *testing.T) {
112 + t.Parallel()
113 + node := makeDaemon(t)
114 +
115 + cidV0 := node.PipeStrToIPFS("test-block-rm", "block", "put", "--format=v0").Stdout.Trimmed()
116 + require.True(t, strings.HasPrefix(cidV0, "Qm"))
117 +
118 + out := node.IPFS("block", "rm", cidBaseFlag, cidV0).Stdout.Trimmed()
119 + require.Contains(t, out, cidV1Prefix, "removed block should be shown as base16 CIDv1")
120 + require.NotContains(t, out, "Qm", "removed block should not contain CIDv0")
121 + })
122 +
123 + t.Run("dag stat respects --cid-base", func(t *testing.T) {
124 + t.Parallel()
125 + node := makeDaemon(t)
126 +
127 + // ipfs add creates dag-pb blocks with CIDv0 by default
128 + cidV0 := node.IPFSAddStr("test-dag-stat", "--pin=false")
129 + require.True(t, strings.HasPrefix(cidV0, "Qm"))
130 +
131 + // JSON output without --cid-base has CIDv0
132 + out := node.IPFS("dag", "stat", "--progress=false", "--enc=json", cidV0).Stdout.Trimmed()
133 + var data struct {
134 + DagStats []struct{ Cid string } `json:"DagStats"`
135 + }
136 + require.NoError(t, json.Unmarshal([]byte(out), &data))
137 + require.True(t, strings.HasPrefix(data.DagStats[0].Cid, "Qm"))
138 +
139 + // JSON output with --cid-base has CIDv1
140 + out = node.IPFS("dag", "stat", "--progress=false", "--enc=json", cidBaseFlag, cidV0).Stdout.Trimmed()
141 + require.NoError(t, json.Unmarshal([]byte(out), &data))
142 + require.True(t, strings.HasPrefix(data.DagStats[0].Cid, cidV1Prefix), "expected base16 CIDv1 in dag stat, got %s", data.DagStats[0].Cid)
143 + })
144 +
145 + t.Run("object patch add-link respects --cid-base", func(t *testing.T) {
146 + t.Parallel()
147 + node := makeDaemon(t)
148 +
149 + // Parent must be a directory for add-link to work
150 + node.IPFS("files", "mkdir", "/patch-add")
151 + parent := node.IPFS("files", "stat", "--hash", "/patch-add").Stdout.Trimmed()
152 + child := node.IPFSAddStr("child", "--pin=false")
153 +
154 + // Without --cid-base: CIDv0
155 + cidV0 := node.IPFS("object", "patch", "add-link", parent, "link", child).Stdout.Trimmed()
156 + require.True(t, strings.HasPrefix(cidV0, "Qm"), "expected CIDv0, got %s", cidV0)
157 +
158 + // With --cid-base: CIDv1
159 + cidV1 := node.IPFS("object", "patch", "add-link", cidBaseFlag, parent, "link", child).Stdout.Trimmed()
160 + require.True(t, strings.HasPrefix(cidV1, cidV1Prefix), "expected base16 CIDv1, got %s", cidV1)
161 + })
162 +
163 + t.Run("object patch rm-link respects --cid-base", func(t *testing.T) {
164 + t.Parallel()
165 + node := makeDaemon(t)
166 +
167 + node.IPFS("files", "mkdir", "/patch-rm")
168 + parent := node.IPFS("files", "stat", "--hash", "/patch-rm").Stdout.Trimmed()
169 + child := node.IPFSAddStr("child", "--pin=false")
170 +
171 + linked := node.IPFS("object", "patch", "add-link", parent, "link", child).Stdout.Trimmed()
172 +
173 + cidV1 := node.IPFS("object", "patch", "rm-link", cidBaseFlag, linked, "link").Stdout.Trimmed()
174 + require.True(t, strings.HasPrefix(cidV1, cidV1Prefix), "expected base16 CIDv1, got %s", cidV1)
175 + })
176 +
177 + t.Run("refs local respects --cid-base", func(t *testing.T) {
178 + t.Parallel()
179 + node := makeDaemon(t)
180 +
181 + node.IPFSAddStr("refs-local-test", "--pin=false")
182 +
183 + lines := node.IPFS("refs", "local", cidBaseFlag).Stdout.Lines()
184 + for _, line := range lines {
185 + if line == "" {
186 + continue
187 + }
188 + require.True(t, strings.HasPrefix(line, cidV1Prefix), "expected base16 CID, got %s", line)
189 + }
190 + })
191 +
192 + t.Run("object diff respects --cid-base", func(t *testing.T) {
193 + t.Parallel()
194 + node := makeDaemon(t)
195 +
196 + cidA := node.IPFSAddStr("aaa", "--pin=false")
197 + cidB := node.IPFSAddStr("bbb", "--pin=false")
198 +
199 + // Create two directories with different children
200 + node.IPFS("files", "mkdir", "/diff-a")
201 + node.IPFS("files", "cp", "/ipfs/"+cidA, "/diff-a/file")
202 + dirA := node.IPFS("files", "stat", "--hash", "/diff-a").Stdout.Trimmed()
203 +
204 + node.IPFS("files", "mkdir", "/diff-b")
205 + node.IPFS("files", "cp", "/ipfs/"+cidB, "/diff-b/file")
206 + dirB := node.IPFS("files", "stat", "--hash", "/diff-b").Stdout.Trimmed()
207 +
208 + // Without --cid-base: CIDs in diff output are CIDv0
209 + out := node.IPFS("object", "diff", dirA, dirB).Stdout.Trimmed()
210 + require.Contains(t, out, "Qm")
211 +
212 + // With --cid-base: CIDs in diff output should be base16
213 + out = node.IPFS("object", "diff", cidBaseFlag, dirA, dirB).Stdout.Trimmed()
214 + require.Contains(t, out, cidV1Prefix, "expected base16 CIDs in diff output")
215 + require.NotContains(t, out, "Qm", "should not contain CIDv0 in diff output")
216 + })
217 +}