Skip to content

Commit c402af5

Browse files
committed
test: legacy-cid-v1, UnixFSChunker, UnixFSFileMaxLinks
basic smoke test confirming UnixFSFileMaxLinks sets to 'ipfs add' defaults and informs the DAG shape
1 parent c5b64cf commit c402af5

File tree

4 files changed

+166
-2
lines changed

4 files changed

+166
-2
lines changed

test/cli/add_test.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/ipfs/kubo/config"
77
"github.com/ipfs/kubo/test/cli/harness"
8+
"github.com/stretchr/testify/assert"
89
"github.com/stretchr/testify/require"
910
)
1011

@@ -106,13 +107,53 @@ func TestAdd(t *testing.T) {
106107
require.Equal(t, shortStringCidV0, cidStr)
107108
})
108109

109-
t.Run("ipfs init --profile=legacy-cid-v1 produces modern CIDv1", func(t *testing.T) {
110+
t.Run("ipfs init --profile=legacy-cid-v0 applies UnixFSChunker=size-262144 and UnixFSFileMaxLinks=174", func(t *testing.T) {
111+
t.Parallel()
112+
node := harness.NewT(t).NewNode().Init("--profile=legacy-cid-v0")
113+
node.StartDaemon()
114+
defer node.StopDaemon()
115+
116+
// Add 44544KiB file:
117+
// 174 * 256KiB should fit in single DAG layer
118+
cidStr := node.IPFSAddFromSeed("44544KiB", "v0-seed")
119+
root, err := node.InspectPBNode(cidStr)
120+
assert.NoError(t, err)
121+
require.Equal(t, 174, len(root.Links))
122+
123+
// add 256KiB (one more block), it should force rebalancing DAG and moving most to second layer
124+
cidStr = node.IPFSAddFromSeed("44800KiB", "v0-seed")
125+
root, err = node.InspectPBNode(cidStr)
126+
assert.NoError(t, err)
127+
require.Equal(t, 2, len(root.Links))
128+
})
129+
130+
t.Run("ipfs init --profile=legacy-cid-v1 produces CIDv1 with raw leaves", func(t *testing.T) {
110131
t.Parallel()
111132
node := harness.NewT(t).NewNode().Init("--profile=legacy-cid-v1")
112133
node.StartDaemon()
113134
defer node.StopDaemon()
114135

115136
cidStr := node.IPFSAddStr(shortString)
116-
require.Equal(t, shortStringCidV1, cidStr)
137+
require.Equal(t, shortStringCidV1, cidStr) // raw leaf
138+
})
139+
140+
t.Run("ipfs init --profile=legacy-cid-v1 applies UnixFSChunker=size-1048576 and UnixFSFileMaxLinks=174", func(t *testing.T) {
141+
t.Parallel()
142+
node := harness.NewT(t).NewNode().Init("--profile=legacy-cid-v1")
143+
node.StartDaemon()
144+
defer node.StopDaemon()
145+
146+
// Add 174MiB file:
147+
// 174 * 1MiB should fit in single layer
148+
cidStr := node.IPFSAddFromSeed("174MiB", "v1-seed")
149+
root, err := node.InspectPBNode(cidStr)
150+
assert.NoError(t, err)
151+
require.Equal(t, 174, len(root.Links))
152+
153+
// add +1MiB (one more block), it should force rebalancing DAG and moving most to second layer
154+
cidStr = node.IPFSAddFromSeed("175MiB", "v1-seed")
155+
root, err = node.InspectPBNode(cidStr)
156+
assert.NoError(t, err)
157+
require.Equal(t, 2, len(root.Links))
117158
})
118159
}

test/cli/harness/ipfs.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,17 @@ func (n *Node) IPFSAddStr(content string, args ...string) string {
7676
return n.IPFSAdd(strings.NewReader(content), args...)
7777
}
7878

79+
// IPFSAddDeterministic produces a CID of a file of a certain size, filled with deterministically generated bytes based on some seed.
80+
// This ensures deterministic CID on the other end, that can be used in tests.
81+
func (n *Node) IPFSAddFromSeed(size string, seed string, args ...string) string {
82+
log.Debugf("node %d adding %s of deterministic pseudo-random data with seed %q and args: %v", n.ID, size, seed, args)
83+
reader, err := createRandomReader(size, seed)
84+
if err != nil {
85+
panic(err)
86+
}
87+
return n.IPFSAdd(reader, args...)
88+
}
89+
7990
func (n *Node) IPFSAdd(content io.Reader, args ...string) string {
8091
log.Debugf("node %d adding with args: %v", n.ID, args)
8192
fullArgs := []string{"add", "-q"}
@@ -108,3 +119,15 @@ func (n *Node) IPFSDagImport(content io.Reader, cid string, args ...string) erro
108119
})
109120
return res.Err
110121
}
122+
123+
/*
124+
func (n *Node) IPFSDagExport(cid string, car *os.File) error {
125+
log.Debugf("node %d dag export of %s to %q with args: %v", n.ID, cid, car.Name())
126+
res := n.Runner.MustRun(RunRequest{
127+
Path: n.IPFSBin,
128+
Args: []string{"dag", "export", cid},
129+
CmdOpts: []CmdOpt{RunWithStdout(car)},
130+
})
131+
return res.Err
132+
}
133+
*/

test/cli/harness/pbinspect.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package harness
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
)
7+
8+
// InspectPBNode uses dag-json output of 'ipfs dag get' to inspect
9+
// "Logical Format" of DAG-PB as defined in
10+
// https://web.archive.org/web/20250403194752/https://ipld.io/specs/codecs/dag-pb/spec/#logical-format
11+
// (mainly used for inspecting Links without depending on any libraries)
12+
func (n *Node) InspectPBNode(cid string) (PBNode, error) {
13+
log.Debugf("node %d dag get %s as dag-json", n.ID, cid)
14+
15+
var root PBNode
16+
var dagJsonOutput bytes.Buffer
17+
res := n.Runner.MustRun(RunRequest{
18+
Path: n.IPFSBin,
19+
Args: []string{"dag", "get", "--output-codec=dag-json", cid},
20+
CmdOpts: []CmdOpt{RunWithStdout(&dagJsonOutput)},
21+
})
22+
if res.Err != nil {
23+
return root, res.Err
24+
}
25+
26+
err := json.Unmarshal(dagJsonOutput.Bytes(), &root)
27+
if err != nil {
28+
return root, err
29+
}
30+
return root, nil
31+
32+
}
33+
34+
// Define structs to match the JSON for
35+
type PBHash struct {
36+
Slash string `json:"/"`
37+
}
38+
39+
type PBLink struct {
40+
Hash PBHash `json:"Hash"`
41+
Name string `json:"Name"`
42+
Tsize int `json:"Tsize"`
43+
}
44+
45+
type PBData struct {
46+
Slash struct {
47+
Bytes string `json:"bytes"`
48+
} `json:"/"`
49+
}
50+
51+
type PBNode struct {
52+
Data PBData `json:"Data"`
53+
Links []PBLink `json:"Links"`
54+
}

test/cli/harness/random_reader.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package harness
2+
3+
import (
4+
"crypto/sha256"
5+
"io"
6+
7+
"github.com/dustin/go-humanize"
8+
"golang.org/x/crypto/chacha20"
9+
)
10+
11+
type randomReader struct {
12+
cipher *chacha20.Cipher
13+
remaining int64
14+
}
15+
16+
func (r *randomReader) Read(p []byte) (int, error) {
17+
if r.remaining <= 0 {
18+
return 0, io.EOF
19+
}
20+
n := int64(len(p))
21+
if n > r.remaining {
22+
n = r.remaining
23+
}
24+
// Generate random bytes directly into the provided buffer
25+
r.cipher.XORKeyStream(p[:n], make([]byte, n))
26+
r.remaining -= n
27+
return int(n), nil
28+
}
29+
30+
// createRandomReader produces specified number of pseudo-random bytes
31+
// from a seed.
32+
func createRandomReader(sizeStr string, seed string) (io.Reader, error) {
33+
size, err := humanize.ParseBytes(sizeStr)
34+
if err != nil {
35+
return nil, err
36+
}
37+
// Hash the seed string to a 32-byte key for ChaCha20
38+
key := sha256.Sum256([]byte(seed))
39+
// Use ChaCha20 for deterministic random bytes
40+
var nonce [chacha20.NonceSize]byte // Zero nonce for simplicity
41+
cipher, err := chacha20.NewUnauthenticatedCipher(key[:chacha20.KeySize], nonce[:])
42+
if err != nil {
43+
return nil, err
44+
}
45+
return &randomReader{cipher: cipher, remaining: int64(size)}, nil
46+
}

0 commit comments

Comments
 (0)