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
+}