feat(config): validate Import config at daemon startup (#10957)
validates Import configuration fields to prevent invalid values: - CidVersion: must be 0 or 1 - UnixFSFileMaxLinks: must be positive - UnixFSDirectoryMaxLinks: must be non-negative - UnixFSHAMTDirectoryMaxFanout: power of 2, multiple of 8, ≤ 1024 - BatchMaxNodes/BatchMaxSize: must be positive - UnixFSChunker: validates format patterns - HashFunction: must be allowed by verifcid
Marcin Rataj committed
Sep 9, 2025 at 01:53 UTC
3e1e7d17fb951575a975915cea6132857c769d01
4 files changed
+570
-3
config/import.go
+135
@@ -1,8 +1,14 @@
1
package config
2
3
import (
4
+ "fmt"
5
+ "strconv"
6
+ "strings"
7
+
8
"github.com/ipfs/boxo/ipld/unixfs/importer/helpers"
9
"github.com/ipfs/boxo/ipld/unixfs/io"
10
+ "github.com/ipfs/boxo/verifcid"
11
+ mh "github.com/multiformats/go-multihash"
12
)
13
14
const (
@@ -43,3 +49,132 @@ type Import struct {
49
BatchMaxNodes OptionalInteger
50
BatchMaxSize OptionalInteger
51
}
52
+
53
+// ValidateImportConfig validates the Import configuration according to UnixFS spec requirements.
54
+// See: https://specs.ipfs.tech/unixfs/#hamt-structure-and-parameters
55
+func ValidateImportConfig(cfg *Import) error {
56
+ // Validate CidVersion
57
+ if !cfg.CidVersion.IsDefault() {
58
+ cidVer := cfg.CidVersion.WithDefault(DefaultCidVersion)
59
+ if cidVer != 0 && cidVer != 1 {
60
+ return fmt.Errorf("Import.CidVersion must be 0 or 1, got %d", cidVer)
61
+ }
62
+ }
63
+
64
+ // Validate UnixFSFileMaxLinks
65
+ if !cfg.UnixFSFileMaxLinks.IsDefault() {
66
+ maxLinks := cfg.UnixFSFileMaxLinks.WithDefault(DefaultUnixFSFileMaxLinks)
67
+ if maxLinks <= 0 {
68
+ return fmt.Errorf("Import.UnixFSFileMaxLinks must be positive, got %d", maxLinks)
69
+ }
70
+ }
71
+
72
+ // Validate UnixFSDirectoryMaxLinks
73
+ if !cfg.UnixFSDirectoryMaxLinks.IsDefault() {
74
+ maxLinks := cfg.UnixFSDirectoryMaxLinks.WithDefault(DefaultUnixFSDirectoryMaxLinks)
75
+ if maxLinks < 0 {
76
+ return fmt.Errorf("Import.UnixFSDirectoryMaxLinks must be non-negative, got %d", maxLinks)
77
+ }
78
+ }
79
+
80
+ // Validate UnixFSHAMTDirectoryMaxFanout if set
81
+ if !cfg.UnixFSHAMTDirectoryMaxFanout.IsDefault() {
82
+ fanout := cfg.UnixFSHAMTDirectoryMaxFanout.WithDefault(DefaultUnixFSHAMTDirectoryMaxFanout)
83
+
84
+ // Check all requirements: fanout < 8 covers both non-positive and non-multiple of 8
85
+ // Combined with power of 2 check and max limit, this ensures valid values: 8, 16, 32, 64, 128, 256, 512, 1024
86
+ if fanout < 8 || !isPowerOfTwo(fanout) || fanout > 1024 {
87
+ return fmt.Errorf("Import.UnixFSHAMTDirectoryMaxFanout must be a positive power of 2, multiple of 8, and not exceed 1024 (got %d)", fanout)
88
+ }
89
+ }
90
+
91
+ // Validate BatchMaxNodes
92
+ if !cfg.BatchMaxNodes.IsDefault() {
93
+ maxNodes := cfg.BatchMaxNodes.WithDefault(DefaultBatchMaxNodes)
94
+ if maxNodes <= 0 {
95
+ return fmt.Errorf("Import.BatchMaxNodes must be positive, got %d", maxNodes)
96
+ }
97
+ }
98
+
99
+ // Validate BatchMaxSize
100
+ if !cfg.BatchMaxSize.IsDefault() {
101
+ maxSize := cfg.BatchMaxSize.WithDefault(DefaultBatchMaxSize)
102
+ if maxSize <= 0 {
103
+ return fmt.Errorf("Import.BatchMaxSize must be positive, got %d", maxSize)
104
+ }
105
+ }
106
+
107
+ // Validate UnixFSChunker format
108
+ if !cfg.UnixFSChunker.IsDefault() {
109
+ chunker := cfg.UnixFSChunker.WithDefault(DefaultUnixFSChunker)
110
+ if !isValidChunker(chunker) {
111
+ return fmt.Errorf("Import.UnixFSChunker invalid format: %q (expected \"size-<bytes>\", \"rabin-<min>-<avg>-<max>\", or \"buzhash\")", chunker)
112
+ }
113
+ }
114
+
115
+ // Validate HashFunction
116
+ if !cfg.HashFunction.IsDefault() {
117
+ hashFunc := cfg.HashFunction.WithDefault(DefaultHashFunction)
118
+ hashCode, ok := mh.Names[strings.ToLower(hashFunc)]
119
+ if !ok {
120
+ return fmt.Errorf("Import.HashFunction unrecognized: %q", hashFunc)
121
+ }
122
+ // Check if the hash is allowed by verifcid
123
+ if !verifcid.DefaultAllowlist.IsAllowed(hashCode) {
124
+ return fmt.Errorf("Import.HashFunction %q is not allowed for use in IPFS", hashFunc)
125
+ }
126
+ }
127
+
128
+ return nil
129
+}
130
+
131
+// isPowerOfTwo checks if a number is a power of 2
132
+func isPowerOfTwo(n int64) bool {
133
+ return n > 0 && (n&(n-1)) == 0
134
+}
135
+
136
+// isValidChunker validates chunker format
137
+func isValidChunker(chunker string) bool {
138
+ if chunker == "buzhash" {
139
+ return true
140
+ }
141
+
142
+ // Check for size-<bytes> format
143
+ if strings.HasPrefix(chunker, "size-") {
144
+ sizeStr := strings.TrimPrefix(chunker, "size-")
145
+ if sizeStr == "" {
146
+ return false
147
+ }
148
+ // Check if it's a valid positive integer (no negative sign allowed)
149
+ if sizeStr[0] == '-' {
150
+ return false
151
+ }
152
+ size, err := strconv.Atoi(sizeStr)
153
+ // Size must be positive (not zero)
154
+ return err == nil && size > 0
155
+ }
156
+
157
+ // Check for rabin-<min>-<avg>-<max> format
158
+ if strings.HasPrefix(chunker, "rabin-") {
159
+ parts := strings.Split(chunker, "-")
160
+ if len(parts) != 4 {
161
+ return false
162
+ }
163
+
164
+ // Parse and validate min, avg, max values
165
+ values := make([]int, 3)
166
+ for i := 0; i < 3; i++ {
167
+ val, err := strconv.Atoi(parts[i+1])
168
+ if err != nil {
169
+ return false
170
+ }
171
+ values[i] = val
172
+ }
173
+
174
+ // Validate ordering: min <= avg <= max
175
+ min, avg, max := values[0], values[1], values[2]
176
+ return min <= avg && avg <= max
177
+ }
178
+
179
+ return false
180
+}
config/import_test.go
new
+408
@@ -0,0 +1,408 @@
1
+package config
2
+
3
+import (
4
+ "strings"
5
+ "testing"
6
+
7
+ mh "github.com/multiformats/go-multihash"
8
+)
9
+
10
+func TestValidateImportConfig_HAMTFanout(t *testing.T) {
11
+ tests := []struct {
12
+ name string
13
+ fanout int64
14
+ wantErr bool
15
+ errMsg string
16
+ }{
17
+ // Valid values - powers of 2, multiples of 8, and <= 1024
18
+ {name: "valid 8", fanout: 8, wantErr: false},
19
+ {name: "valid 16", fanout: 16, wantErr: false},
20
+ {name: "valid 32", fanout: 32, wantErr: false},
21
+ {name: "valid 64", fanout: 64, wantErr: false},
22
+ {name: "valid 128", fanout: 128, wantErr: false},
23
+ {name: "valid 256", fanout: 256, wantErr: false},
24
+ {name: "valid 512", fanout: 512, wantErr: false},
25
+ {name: "valid 1024", fanout: 1024, wantErr: false},
26
+
27
+ // Invalid values - not powers of 2
28
+ {name: "invalid 7", fanout: 7, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
29
+ {name: "invalid 15", fanout: 15, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
30
+ {name: "invalid 100", fanout: 100, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
31
+ {name: "invalid 257", fanout: 257, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
32
+ {name: "invalid 1000", fanout: 1000, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
33
+
34
+ // Invalid values - powers of 2 but not multiples of 8
35
+ {name: "invalid 1", fanout: 1, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
36
+ {name: "invalid 2", fanout: 2, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
37
+ {name: "invalid 4", fanout: 4, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
38
+
39
+ // Invalid values - exceeds 1024
40
+ {name: "invalid 2048", fanout: 2048, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
41
+ {name: "invalid 4096", fanout: 4096, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
42
+
43
+ // Invalid values - negative or zero
44
+ {name: "invalid 0", fanout: 0, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
45
+ {name: "invalid -8", fanout: -8, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
46
+ {name: "invalid -256", fanout: -256, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
47
+ }
48
+
49
+ for _, tt := range tests {
50
+ t.Run(tt.name, func(t *testing.T) {
51
+ cfg := &Import{
52
+ UnixFSHAMTDirectoryMaxFanout: *NewOptionalInteger(tt.fanout),
53
+ }
54
+
55
+ err := ValidateImportConfig(cfg)
56
+
57
+ if tt.wantErr {
58
+ if err == nil {
59
+ t.Errorf("ValidateImportConfig() expected error for fanout=%d, got nil", tt.fanout)
60
+ } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
61
+ t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
62
+ }
63
+ } else {
64
+ if err != nil {
65
+ t.Errorf("ValidateImportConfig() unexpected error for fanout=%d: %v", tt.fanout, err)
66
+ }
67
+ }
68
+ })
69
+ }
70
+}
71
+
72
+func TestValidateImportConfig_CidVersion(t *testing.T) {
73
+ tests := []struct {
74
+ name string
75
+ cidVer int64
76
+ wantErr bool
77
+ errMsg string
78
+ }{
79
+ {name: "valid 0", cidVer: 0, wantErr: false},
80
+ {name: "valid 1", cidVer: 1, wantErr: false},
81
+ {name: "invalid 2", cidVer: 2, wantErr: true, errMsg: "must be 0 or 1"},
82
+ {name: "invalid -1", cidVer: -1, wantErr: true, errMsg: "must be 0 or 1"},
83
+ {name: "invalid 100", cidVer: 100, wantErr: true, errMsg: "must be 0 or 1"},
84
+ }
85
+
86
+ for _, tt := range tests {
87
+ t.Run(tt.name, func(t *testing.T) {
88
+ cfg := &Import{
89
+ CidVersion: *NewOptionalInteger(tt.cidVer),
90
+ }
91
+
92
+ err := ValidateImportConfig(cfg)
93
+
94
+ if tt.wantErr {
95
+ if err == nil {
96
+ t.Errorf("ValidateImportConfig() expected error for cidVer=%d, got nil", tt.cidVer)
97
+ } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
98
+ t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
99
+ }
100
+ } else {
101
+ if err != nil {
102
+ t.Errorf("ValidateImportConfig() unexpected error for cidVer=%d: %v", tt.cidVer, err)
103
+ }
104
+ }
105
+ })
106
+ }
107
+}
108
+
109
+func TestValidateImportConfig_UnixFSFileMaxLinks(t *testing.T) {
110
+ tests := []struct {
111
+ name string
112
+ maxLinks int64
113
+ wantErr bool
114
+ errMsg string
115
+ }{
116
+ {name: "valid 1", maxLinks: 1, wantErr: false},
117
+ {name: "valid 174", maxLinks: 174, wantErr: false},
118
+ {name: "valid 1000", maxLinks: 1000, wantErr: false},
119
+ {name: "invalid 0", maxLinks: 0, wantErr: true, errMsg: "must be positive"},
120
+ {name: "invalid -1", maxLinks: -1, wantErr: true, errMsg: "must be positive"},
121
+ }
122
+
123
+ for _, tt := range tests {
124
+ t.Run(tt.name, func(t *testing.T) {
125
+ cfg := &Import{
126
+ UnixFSFileMaxLinks: *NewOptionalInteger(tt.maxLinks),
127
+ }
128
+
129
+ err := ValidateImportConfig(cfg)
130
+
131
+ if tt.wantErr {
132
+ if err == nil {
133
+ t.Errorf("ValidateImportConfig() expected error for maxLinks=%d, got nil", tt.maxLinks)
134
+ } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
135
+ t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
136
+ }
137
+ } else {
138
+ if err != nil {
139
+ t.Errorf("ValidateImportConfig() unexpected error for maxLinks=%d: %v", tt.maxLinks, err)
140
+ }
141
+ }
142
+ })
143
+ }
144
+}
145
+
146
+func TestValidateImportConfig_UnixFSDirectoryMaxLinks(t *testing.T) {
147
+ tests := []struct {
148
+ name string
149
+ maxLinks int64
150
+ wantErr bool
151
+ errMsg string
152
+ }{
153
+ {name: "valid 0", maxLinks: 0, wantErr: false}, // 0 means no limit
154
+ {name: "valid 1", maxLinks: 1, wantErr: false},
155
+ {name: "valid 1000", maxLinks: 1000, wantErr: false},
156
+ {name: "invalid -1", maxLinks: -1, wantErr: true, errMsg: "must be non-negative"},
157
+ {name: "invalid -100", maxLinks: -100, wantErr: true, errMsg: "must be non-negative"},
158
+ }
159
+
160
+ for _, tt := range tests {
161
+ t.Run(tt.name, func(t *testing.T) {
162
+ cfg := &Import{
163
+ UnixFSDirectoryMaxLinks: *NewOptionalInteger(tt.maxLinks),
164
+ }
165
+
166
+ err := ValidateImportConfig(cfg)
167
+
168
+ if tt.wantErr {
169
+ if err == nil {
170
+ t.Errorf("ValidateImportConfig() expected error for maxLinks=%d, got nil", tt.maxLinks)
171
+ } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
172
+ t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
173
+ }
174
+ } else {
175
+ if err != nil {
176
+ t.Errorf("ValidateImportConfig() unexpected error for maxLinks=%d: %v", tt.maxLinks, err)
177
+ }
178
+ }
179
+ })
180
+ }
181
+}
182
+
183
+func TestValidateImportConfig_BatchMax(t *testing.T) {
184
+ tests := []struct {
185
+ name string
186
+ maxNodes int64
187
+ maxSize int64
188
+ wantErr bool
189
+ errMsg string
190
+ }{
191
+ {name: "valid nodes 1", maxNodes: 1, maxSize: -999, wantErr: false},
192
+ {name: "valid nodes 128", maxNodes: 128, maxSize: -999, wantErr: false},
193
+ {name: "valid size 1", maxNodes: -999, maxSize: 1, wantErr: false},
194
+ {name: "valid size 20MB", maxNodes: -999, maxSize: 20 << 20, wantErr: false},
195
+ {name: "invalid nodes 0", maxNodes: 0, maxSize: -999, wantErr: true, errMsg: "BatchMaxNodes must be positive"},
196
+ {name: "invalid nodes -1", maxNodes: -1, maxSize: -999, wantErr: true, errMsg: "BatchMaxNodes must be positive"},
197
+ {name: "invalid size 0", maxNodes: -999, maxSize: 0, wantErr: true, errMsg: "BatchMaxSize must be positive"},
198
+ {name: "invalid size -1", maxNodes: -999, maxSize: -1, wantErr: true, errMsg: "BatchMaxSize must be positive"},
199
+ }
200
+
201
+ for _, tt := range tests {
202
+ t.Run(tt.name, func(t *testing.T) {
203
+ cfg := &Import{}
204
+ if tt.maxNodes != -999 {
205
+ cfg.BatchMaxNodes = *NewOptionalInteger(tt.maxNodes)
206
+ }
207
+ if tt.maxSize != -999 {
208
+ cfg.BatchMaxSize = *NewOptionalInteger(tt.maxSize)
209
+ }
210
+
211
+ err := ValidateImportConfig(cfg)
212
+
213
+ if tt.wantErr {
214
+ if err == nil {
215
+ t.Errorf("ValidateImportConfig() expected error, got nil")
216
+ } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
217
+ t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
218
+ }
219
+ } else {
220
+ if err != nil {
221
+ t.Errorf("ValidateImportConfig() unexpected error: %v", err)
222
+ }
223
+ }
224
+ })
225
+ }
226
+}
227
+
228
+func TestValidateImportConfig_UnixFSChunker(t *testing.T) {
229
+ tests := []struct {
230
+ name string
231
+ chunker string
232
+ wantErr bool
233
+ errMsg string
234
+ }{
235
+ {name: "valid size-262144", chunker: "size-262144", wantErr: false},
236
+ {name: "valid size-1", chunker: "size-1", wantErr: false},
237
+ {name: "valid size-1048576", chunker: "size-1048576", wantErr: false},
238
+ {name: "valid rabin", chunker: "rabin-128-256-512", wantErr: false},
239
+ {name: "valid rabin min", chunker: "rabin-16-32-64", wantErr: false},
240
+ {name: "valid buzhash", chunker: "buzhash", wantErr: false},
241
+ {name: "invalid size-", chunker: "size-", wantErr: true, errMsg: "invalid format"},
242
+ {name: "invalid size-abc", chunker: "size-abc", wantErr: true, errMsg: "invalid format"},
243
+ {name: "invalid rabin-", chunker: "rabin-", wantErr: true, errMsg: "invalid format"},
244
+ {name: "invalid rabin-128", chunker: "rabin-128", wantErr: true, errMsg: "invalid format"},
245
+ {name: "invalid rabin-128-256", chunker: "rabin-128-256", wantErr: true, errMsg: "invalid format"},
246
+ {name: "invalid rabin-a-b-c", chunker: "rabin-a-b-c", wantErr: true, errMsg: "invalid format"},
247
+ {name: "invalid unknown", chunker: "unknown", wantErr: true, errMsg: "invalid format"},
248
+ {name: "invalid empty", chunker: "", wantErr: true, errMsg: "invalid format"},
249
+ }
250
+
251
+ for _, tt := range tests {
252
+ t.Run(tt.name, func(t *testing.T) {
253
+ cfg := &Import{
254
+ UnixFSChunker: *NewOptionalString(tt.chunker),
255
+ }
256
+
257
+ err := ValidateImportConfig(cfg)
258
+
259
+ if tt.wantErr {
260
+ if err == nil {
261
+ t.Errorf("ValidateImportConfig() expected error for chunker=%s, got nil", tt.chunker)
262
+ } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
263
+ t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
264
+ }
265
+ } else {
266
+ if err != nil {
267
+ t.Errorf("ValidateImportConfig() unexpected error for chunker=%s: %v", tt.chunker, err)
268
+ }
269
+ }
270
+ })
271
+ }
272
+}
273
+
274
+func TestValidateImportConfig_HashFunction(t *testing.T) {
275
+ tests := []struct {
276
+ name string
277
+ hashFunc string
278
+ wantErr bool
279
+ errMsg string
280
+ }{
281
+ {name: "valid sha2-256", hashFunc: "sha2-256", wantErr: false},
282
+ {name: "valid sha2-512", hashFunc: "sha2-512", wantErr: false},
283
+ {name: "valid sha3-256", hashFunc: "sha3-256", wantErr: false},
284
+ {name: "valid blake2b-256", hashFunc: "blake2b-256", wantErr: false},
285
+ {name: "valid blake3", hashFunc: "blake3", wantErr: false},
286
+ {name: "invalid unknown", hashFunc: "unknown-hash", wantErr: true, errMsg: "unrecognized"},
287
+ {name: "invalid empty", hashFunc: "", wantErr: true, errMsg: "unrecognized"},
288
+ }
289
+
290
+ // Check for hashes that exist but are not allowed
291
+ // MD5 should exist but not be allowed
292
+ if code, ok := mh.Names["md5"]; ok {
293
+ tests = append(tests, struct {
294
+ name string
295
+ hashFunc string
296
+ wantErr bool
297
+ errMsg string
298
+ }{name: "md5 not allowed", hashFunc: "md5", wantErr: true, errMsg: "not allowed"})
299
+ _ = code // use the variable
300
+ }
301
+
302
+ for _, tt := range tests {
303
+ t.Run(tt.name, func(t *testing.T) {
304
+ cfg := &Import{
305
+ HashFunction: *NewOptionalString(tt.hashFunc),
306
+ }
307
+
308
+ err := ValidateImportConfig(cfg)
309
+
310
+ if tt.wantErr {
311
+ if err == nil {
312
+ t.Errorf("ValidateImportConfig() expected error for hashFunc=%s, got nil", tt.hashFunc)
313
+ } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
314
+ t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
315
+ }
316
+ } else {
317
+ if err != nil {
318
+ t.Errorf("ValidateImportConfig() unexpected error for hashFunc=%s: %v", tt.hashFunc, err)
319
+ }
320
+ }
321
+ })
322
+ }
323
+}
324
+
325
+func TestValidateImportConfig_DefaultValue(t *testing.T) {
326
+ // Test that default (unset) value doesn't trigger validation
327
+ cfg := &Import{}
328
+
329
+ err := ValidateImportConfig(cfg)
330
+ if err != nil {
331
+ t.Errorf("ValidateImportConfig() unexpected error for default config: %v", err)
332
+ }
333
+}
334
+
335
+func TestIsValidChunker(t *testing.T) {
336
+ tests := []struct {
337
+ chunker string
338
+ want bool
339
+ }{
340
+ {"buzhash", true},
341
+ {"size-262144", true},
342
+ {"size-1", true},
343
+ {"size-0", false}, // 0 is not valid - must be positive
344
+ {"size-9999999", true},
345
+ {"rabin-128-256-512", true},
346
+ {"rabin-16-32-64", true},
347
+ {"rabin-1-2-3", true},
348
+ {"rabin-512-256-128", false}, // Invalid ordering: min > avg > max
349
+ {"rabin-256-128-512", false}, // Invalid ordering: min > avg
350
+ {"rabin-128-512-256", false}, // Invalid ordering: avg > max
351
+
352
+ {"", false},
353
+ {"size-", false},
354
+ {"size-abc", false},
355
+ {"size--1", false},
356
+ {"rabin-", false},
357
+ {"rabin-128", false},
358
+ {"rabin-128-256", false},
359
+ {"rabin-128-256-512-1024", false},
360
+ {"rabin-a-b-c", false},
361
+ {"unknown", false},
362
+ {"buzzhash", false}, // typo
363
+ }
364
+
365
+ for _, tt := range tests {
366
+ t.Run(tt.chunker, func(t *testing.T) {
367
+ if got := isValidChunker(tt.chunker); got != tt.want {
368
+ t.Errorf("isValidChunker(%q) = %v, want %v", tt.chunker, got, tt.want)
369
+ }
370
+ })
371
+ }
372
+}
373
+
374
+func TestIsPowerOfTwo(t *testing.T) {
375
+ tests := []struct {
376
+ n int64
377
+ want bool
378
+ }{
379
+ {0, false},
380
+ {1, true},
381
+ {2, true},
382
+ {3, false},
383
+ {4, true},
384
+ {5, false},
385
+ {6, false},
386
+ {7, false},
387
+ {8, true},
388
+ {16, true},
389
+ {32, true},
390
+ {64, true},
391
+ {100, false},
392
+ {128, true},
393
+ {256, true},
394
+ {512, true},
395
+ {1024, true},
396
+ {2048, true},
397
+ {-1, false},
398
+ {-8, false},
399
+ }
400
+
401
+ for _, tt := range tests {
402
+ t.Run("", func(t *testing.T) {
403
+ if got := isPowerOfTwo(tt.n); got != tt.want {
404
+ t.Errorf("isPowerOfTwo(%d) = %v, want %v", tt.n, got, tt.want)
405
+ }
406
+ })
407
+ }
408
+}
core/node/groups.go
+5
@@ -432,6 +432,11 @@ func IPFS(ctx context.Context, bcfg *BuildCfg) fx.Option {
432
cfg.Import.UnixFSHAMTDirectorySizeThreshold = *cfg.Internal.UnixFSShardingSizeThreshold
433
}
434
435
+ // Validate Import configuration
436
+ if err := config.ValidateImportConfig(&cfg.Import); err != nil {
437
+ return fx.Error(err)
438
+ }
439
+
440
// Auto-sharding settings
441
shardingThresholdString := cfg.Import.UnixFSHAMTDirectorySizeThreshold.WithDefault(config.DefaultUnixFSHAMTDirectorySizeThreshold)
442
shardSingThresholdInt, err := humanize.ParseBytes(shardingThresholdString)
docs/config.md
+22
-3
@@ -3133,6 +3133,8 @@ Note that using flags will override the options defined here.
3133
3134
The default CID version. Commands affected: `ipfs add`.
3135
3136
+Must be either 0 or 1. CIDv0 uses SHA2-256 only, while CIDv1 supports multiple hash functions.
3137
+
3138
Default: `0`
3139
3140
Type: `optionalInteger`
@@ -3149,6 +3151,11 @@ Type: `flag`
3151
3152
The default UnixFS chunker. Commands affected: `ipfs add`.
3153
3154
+Valid formats:
3155
+- `size-<bytes>` - fixed size chunker
3156
+- `rabin-<min>-<avg>-<max>` - rabin fingerprint chunker
3157
+- `buzhash` - buzhash chunker
3158
+
3159
Default: `size-262144`
3160
3161
Type: `optionalString`
@@ -3157,6 +3164,10 @@ Type: `optionalString`
3164
3165
The default hash function. Commands affected: `ipfs add`, `ipfs block put`, `ipfs dag put`.
3166
3167
+Must be a valid multihash name (e.g., `sha2-256`, `blake3`) and must be allowed for use in IPFS according to security constraints.
3168
+
3169
+Run `ipfs cid hashes --supported` to see the full list of allowed hash functions.
3170
+
3171
Default: `sha2-256`
3172
3173
Type: `optionalString`
@@ -3167,6 +3178,8 @@ The maximum number of nodes in a write-batch. The total size of the batch is lim
3178
3179
Increasing this will batch more items together when importing data with `ipfs dag import`, which can speed things up.
3180
3181
+Must be positive (> 0). Setting to 0 would cause immediate batching after each node, which is inefficient.
3182
+
3183
Default: `128`
3184
3185
Type: `optionalInteger`
@@ -3177,6 +3190,8 @@ The maximum size of a single write-batch (computed as the sum of the sizes of th
3190
3191
Increasing this will batch more items together when importing data with `ipfs dag import`, which can speed things up.
3192
3193
+Must be positive (> 0). Setting to 0 would cause immediate batching after any data, which is inefficient.
3194
+
3195
Default: `20971520` (20MiB)
3196
3197
Type: `optionalInteger`
@@ -3189,6 +3204,8 @@ when building the DAG while importing.
3204
This setting controls both the fanout in files that are chunked into several
3205
blocks and grouped as a Unixfs (dag-pb) DAG.
3206
3207
+Must be positive (> 0). Zero or negative values would break file DAG construction.
3208
+
3209
Default: `174`
3210
3211
Type: `optionalInteger`
@@ -3208,6 +3225,8 @@ This setting will cause basic directories to be converted to HAMTs when they
3225
exceed the maximum number of children. This happens transparently during the
3226
add process. The fanout of HAMT nodes is controlled by `MaxHAMTFanout`.
3227
3228
+Must be non-negative (>= 0). Zero means no limit, negative values are invalid.
3229
+
3230
Commands affected: `ipfs add`
3231
3232
Default: `0` (no limit, because [`Import.UnixFSHAMTDirectorySizeThreshold`](#importunixfshamtdirectorysizethreshold) triggers controls when to switch to HAMT sharding when a directory grows too big)
@@ -3216,15 +3235,15 @@ Type: `optionalInteger`
3235
3236
### `Import.UnixFSHAMTDirectoryMaxFanout`
3237
3219
-The maximum number of children that a node part of a Unixfs HAMT directory
3238
+The maximum number of children that a node part of a UnixFS HAMT directory
3239
(aka sharded directory) can have.
3240
3241
HAMT directories have unlimited children and are used when basic directories
3223
-become too big or reach `MaxLinks`. A HAMT is a structure made of unixfs
3242
+become too big or reach `MaxLinks`. A HAMT is a structure made of UnixFS
3243
nodes that store the list of elements in the folder. This option controls the
3244
maximum number of children that the HAMT nodes can have.
3245
3227
-Needs to be a power of two (shard entry size) and multiple of 8 (bitfield size).
3246
+According to the [UnixFS specification](https://specs.ipfs.tech/unixfs/#hamt-structure-and-parameters), this value must be a power of 2, a multiple of 8 (for byte-aligned bitfields), and not exceed 1024 (to prevent denial-of-service attacks).
3247
3248
Commands affected: `ipfs add`, `ipfs daemon` (globally overrides [`boxo/ipld/unixfs/io.DefaultShardWidth`](https://github.com/ipfs/boxo/blob/6c5a07602aed248acc86598f30ab61923a54a83e/ipld/unixfs/io/directory.go#L30C5-L30C22))
3249