5
"testing"
6
7
dag "github.com/ipfs/boxo/ipld/merkledag"
8
+ ft "github.com/ipfs/boxo/ipld/unixfs"
9
"github.com/ipfs/boxo/path"
10
ipld "github.com/ipfs/go-ipld-format"
11
iface "github.com/ipfs/kubo/core/coreiface"
23
24
t.Run("TestObjectAddLink", tp.TestObjectAddLink)
25
t.Run("TestObjectAddLinkCreate", tp.TestObjectAddLinkCreate)
26
+ t.Run("TestObjectAddLinkValidation", tp.TestObjectAddLinkValidation)
27
t.Run("TestObjectRmLink", tp.TestObjectRmLink)
28
+ t.Run("TestObjectRmLinkValidation", tp.TestObjectRmLinkValidation)
29
t.Run("TestDiffTest", tp.TestDiffTest)
30
}
31
61
},
62
})
63
61
- p3, err := api.Object().AddLink(ctx, p2, "abc", p2)
64
+ // Raw dag-pb nodes require SkipUnixFSValidation since they have no UnixFS metadata
65
+ p3, err := api.Object().AddLink(ctx, p2, "abc", p2, opt.Object.SkipUnixFSValidation(true))
66
require.NoError(t, err)
67
68
nd, err := api.Dag().Get(ctx, p3.RootCid())
88
},
89
})
90
87
- _, err = api.Object().AddLink(ctx, p2, "abc/d", p2)
91
+ // Raw dag-pb nodes require SkipUnixFSValidation since they have no UnixFS metadata
92
+ _, err = api.Object().AddLink(ctx, p2, "abc/d", p2, opt.Object.SkipUnixFSValidation(true))
93
require.ErrorContains(t, err, "no link by that name")
94
90
- p3, err := api.Object().AddLink(ctx, p2, "abc/d", p2, opt.Object.Create(true))
95
+ p3, err := api.Object().AddLink(ctx, p2, "abc/d", p2, opt.Object.Create(true), opt.Object.SkipUnixFSValidation(true))
96
require.NoError(t, err)
97
98
nd, err := api.Dag().Get(ctx, p3.RootCid())
104
require.Equal(t, "bar", links[1].Name)
105
}
106
107
+// TestObjectAddLinkValidation verifies that AddLink rejects non-directory
108
+// nodes by default, preventing the data-loss bug in
109
+// https://github.com/ipfs/kubo/issues/7190
110
+func (tp *TestSuite) TestObjectAddLinkValidation(t *testing.T) {
111
+ ctx := t.Context()
112
+ api, err := tp.makeAPI(t, ctx)
113
+ require.NoError(t, err)
114
+
115
+ child := putDagPbNode(t, ctx, api, "child", nil)
116
+
117
+ // UnixFS Directory: allowed
118
+ dirNode := ft.EmptyDirNode()
119
+ err = api.Dag().Add(ctx, dirNode)
120
+ require.NoError(t, err)
121
+ dirPath := path.FromCid(dirNode.Cid())
122
+
123
+ _, err = api.Object().AddLink(ctx, dirPath, "foo", child)
124
+ require.NoError(t, err)
125
+
126
+ // UnixFS File: rejected (would cause data loss on read-back)
127
+ fileNode := ft.EmptyFileNode()
128
+ err = api.Dag().Add(ctx, fileNode)
129
+ require.NoError(t, err)
130
+ filePath := path.FromCid(fileNode.Cid())
131
+
132
+ _, err = api.Object().AddLink(ctx, filePath, "foo", child)
133
+ require.ErrorContains(t, err, "cannot add named links to a UnixFS File node, only Directory nodes support link addition at the dag-pb level")
134
+
135
+ // UnixFS File with SkipUnixFSValidation: allowed (user takes responsibility)
136
+ _, err = api.Object().AddLink(ctx, filePath, "foo", child, opt.Object.SkipUnixFSValidation(true))
137
+ require.NoError(t, err)
138
+
139
+ // HAMTShard: rejected (dag-pb level mutation corrupts HAMT bitfield)
140
+ hamtData, err := ft.HAMTShardData(nil, 256, 0x22)
141
+ require.NoError(t, err)
142
+ hamtNode := new(dag.ProtoNode)
143
+ hamtNode.SetData(hamtData)
144
+ err = api.Dag().Add(ctx, hamtNode)
145
+ require.NoError(t, err)
146
+ hamtPath := path.FromCid(hamtNode.Cid())
147
+
148
+ _, err = api.Object().AddLink(ctx, hamtPath, "foo", child)
149
+ require.ErrorContains(t, err, "cannot add links to a HAMTShard at the dag-pb level (would corrupt the HAMT bitfield); use 'ipfs files' commands instead, or pass --allow-non-unixfs to override")
150
+
151
+ // HAMTShard with SkipUnixFSValidation: allowed
152
+ _, err = api.Object().AddLink(ctx, hamtPath, "foo", child, opt.Object.SkipUnixFSValidation(true))
153
+ require.NoError(t, err)
154
+
155
+ // Raw dag-pb (no UnixFS data): rejected
156
+ rawPb := putDagPbNode(t, ctx, api, "", nil)
157
+
158
+ _, err = api.Object().AddLink(ctx, rawPb, "foo", child)
159
+ require.ErrorContains(t, err, "cannot add named links to a non-UnixFS dag-pb node; pass --allow-non-unixfs to skip validation")
160
+
161
+ // Raw dag-pb with SkipUnixFSValidation: allowed
162
+ _, err = api.Object().AddLink(ctx, rawPb, "foo", child, opt.Object.SkipUnixFSValidation(true))
163
+ require.NoError(t, err)
164
+}
165
+
166
func (tp *TestSuite) TestObjectRmLink(t *testing.T) {
167
ctx := t.Context()
168
api, err := tp.makeAPI(t, ctx)
177
},
178
})
179
116
- p3, err := api.Object().RmLink(ctx, p2, "bar")
180
+ // Raw dag-pb nodes require SkipUnixFSValidation since they have no UnixFS metadata
181
+ p3, err := api.Object().RmLink(ctx, p2, "bar", opt.Object.RmLinkSkipUnixFSValidation(true))
182
require.NoError(t, err)
183
184
nd, err := api.Dag().Get(ctx, p3.RootCid())
188
require.Len(t, links, 0)
189
}
190
191
+// TestObjectRmLinkValidation verifies that RmLink rejects non-directory
192
+// nodes by default, preventing silent DAG corruption.
193
+func (tp *TestSuite) TestObjectRmLinkValidation(t *testing.T) {
194
+ ctx := t.Context()
195
+ api, err := tp.makeAPI(t, ctx)
196
+ require.NoError(t, err)
197
+
198
+ child := putDagPbNode(t, ctx, api, "child", nil)
199
+
200
+ // UnixFS Directory with a link: rm-link allowed
201
+ dirNode := ft.EmptyDirNode()
202
+ childNd, err := api.Dag().Get(ctx, child.RootCid())
203
+ require.NoError(t, err)
204
+ err = dirNode.AddNodeLink("foo", childNd)
205
+ require.NoError(t, err)
206
+ err = api.Dag().Add(ctx, dirNode)
207
+ require.NoError(t, err)
208
+ dirPath := path.FromCid(dirNode.Cid())
209
+
210
+ _, err = api.Object().RmLink(ctx, dirPath, "foo")
211
+ require.NoError(t, err)
212
+
213
+ // UnixFS File: rejected
214
+ fileNode := ft.EmptyFileNode()
215
+ err = api.Dag().Add(ctx, fileNode)
216
+ require.NoError(t, err)
217
+ filePath := path.FromCid(fileNode.Cid())
218
+
219
+ _, err = api.Object().RmLink(ctx, filePath, "foo")
220
+ require.ErrorContains(t, err, "cannot remove links from a UnixFS File node, only Directory nodes support link removal at the dag-pb level")
221
+
222
+ // UnixFS File with SkipUnixFSValidation: allowed
223
+ _, err = api.Object().RmLink(ctx, filePath, "foo", opt.Object.RmLinkSkipUnixFSValidation(true))
224
+ // ErrLinkNotFound is expected since the file has no links, but validation passed
225
+ require.ErrorContains(t, err, "no link by that name")
226
+
227
+ // HAMTShard: rejected
228
+ hamtData, err := ft.HAMTShardData(nil, 256, 0x22)
229
+ require.NoError(t, err)
230
+ hamtNode := new(dag.ProtoNode)
231
+ hamtNode.SetData(hamtData)
232
+ err = api.Dag().Add(ctx, hamtNode)
233
+ require.NoError(t, err)
234
+ hamtPath := path.FromCid(hamtNode.Cid())
235
+
236
+ _, err = api.Object().RmLink(ctx, hamtPath, "foo")
237
+ require.ErrorContains(t, err, "cannot remove links from a HAMTShard at the dag-pb level (would corrupt the HAMT bitfield); use 'ipfs files rm' instead, or pass --allow-non-unixfs to override")
238
+
239
+ // HAMTShard with SkipUnixFSValidation: allowed (validation bypassed)
240
+ _, err = api.Object().RmLink(ctx, hamtPath, "foo", opt.Object.RmLinkSkipUnixFSValidation(true))
241
+ require.ErrorContains(t, err, "no link by that name")
242
+
243
+ // Raw dag-pb (no UnixFS data): rejected
244
+ rawPb := putDagPbNode(t, ctx, api, "", nil)
245
+
246
+ _, err = api.Object().RmLink(ctx, rawPb, "foo")
247
+ require.ErrorContains(t, err, "cannot remove links from a non-UnixFS dag-pb node; pass --allow-non-unixfs to skip validation")
248
+
249
+ // Raw dag-pb with SkipUnixFSValidation: allowed
250
+ _, err = api.Object().RmLink(ctx, rawPb, "foo", opt.Object.RmLinkSkipUnixFSValidation(true))
251
+ require.ErrorContains(t, err, "no link by that name")
252
+}
253
+
254
func (tp *TestSuite) TestDiffTest(t *testing.T) {
255
ctx := t.Context()
256
api, err := tp.makeAPI(t, ctx)