| 1 | package tests |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 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" |
| 12 | opt "github.com/ipfs/kubo/core/coreiface/options" |
| 13 | "github.com/stretchr/testify/require" |
| 14 | ) |
| 15 | |
| 16 | func (tp *TestSuite) TestObject(t *testing.T) { |
| 17 | tp.hasApi(t, func(api iface.CoreAPI) error { |
| 18 | if api.Object() == nil { |
| 19 | return errAPINotImplemented |
| 20 | } |
| 21 | return nil |
| 22 | }) |
| 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 | |
| 32 | func putDagPbNode(t *testing.T, ctx context.Context, api iface.CoreAPI, data string, links []*ipld.Link) path.ImmutablePath { |
| 33 | dagnode := new(dag.ProtoNode) |
| 34 | |
| 35 | if data != "" { |
| 36 | dagnode.SetData([]byte(data)) |
| 37 | } |
| 38 | |
| 39 | if links != nil { |
| 40 | err := dagnode.SetLinks(links) |
| 41 | require.NoError(t, err) |
| 42 | } |
| 43 | |
| 44 | err := api.Dag().Add(ctx, dagnode) |
| 45 | require.NoError(t, err) |
| 46 | |
| 47 | return path.FromCid(dagnode.Cid()) |
| 48 | } |
| 49 | |
| 50 | func (tp *TestSuite) TestObjectAddLink(t *testing.T) { |
| 51 | ctx := t.Context() |
| 52 | api, err := tp.makeAPI(t, ctx) |
| 53 | require.NoError(t, err) |
| 54 | |
| 55 | p1 := putDagPbNode(t, ctx, api, "foo", nil) |
| 56 | p2 := putDagPbNode(t, ctx, api, "bazz", []*ipld.Link{ |
| 57 | { |
| 58 | Name: "bar", |
| 59 | Cid: p1.RootCid(), |
| 60 | Size: 3, |
| 61 | }, |
| 62 | }) |
| 63 | |
| 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()) |
| 69 | require.NoError(t, err) |
| 70 | |
| 71 | links := nd.Links() |
| 72 | require.Len(t, links, 2) |
| 73 | require.Equal(t, "abc", links[0].Name) |
| 74 | require.Equal(t, "bar", links[1].Name) |
| 75 | } |
| 76 | |
| 77 | func (tp *TestSuite) TestObjectAddLinkCreate(t *testing.T) { |
| 78 | ctx := t.Context() |
| 79 | api, err := tp.makeAPI(t, ctx) |
| 80 | require.NoError(t, err) |
| 81 | |
| 82 | p1 := putDagPbNode(t, ctx, api, "foo", nil) |
| 83 | p2 := putDagPbNode(t, ctx, api, "bazz", []*ipld.Link{ |
| 84 | { |
| 85 | Name: "bar", |
| 86 | Cid: p1.RootCid(), |
| 87 | Size: 3, |
| 88 | }, |
| 89 | }) |
| 90 | |
| 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 | |
| 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()) |
| 99 | require.NoError(t, err) |
| 100 | |
| 101 | links := nd.Links() |
| 102 | require.Len(t, links, 2) |
| 103 | require.Equal(t, "abc", links[0].Name) |
| 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) |
| 169 | require.NoError(t, err) |
| 170 | |
| 171 | p1 := putDagPbNode(t, ctx, api, "foo", nil) |
| 172 | p2 := putDagPbNode(t, ctx, api, "bazz", []*ipld.Link{ |
| 173 | { |
| 174 | Name: "bar", |
| 175 | Cid: p1.RootCid(), |
| 176 | Size: 3, |
| 177 | }, |
| 178 | }) |
| 179 | |
| 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()) |
| 185 | require.NoError(t, err) |
| 186 | |
| 187 | links := nd.Links() |
| 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) |
| 257 | require.NoError(t, err) |
| 258 | |
| 259 | p1 := putDagPbNode(t, ctx, api, "foo", nil) |
| 260 | p2 := putDagPbNode(t, ctx, api, "bar", nil) |
| 261 | |
| 262 | changes, err := api.Object().Diff(ctx, p1, p2) |
| 263 | require.NoError(t, err) |
| 264 | require.Len(t, changes, 1) |
| 265 | require.Equal(t, iface.DiffMod, changes[0].Type) |
| 266 | require.Equal(t, p1.String(), changes[0].Before.String()) |
| 267 | require.Equal(t, p2.String(), changes[0].After.String()) |
| 268 | } |