Skip to content

Commit 3e1e7d1

Browse files
authored
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
1 parent 049256c commit 3e1e7d1

File tree

4 files changed

+570
-3
lines changed

4 files changed

+570
-3
lines changed

config/import.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package config
22

33
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
48
"github.com/ipfs/boxo/ipld/unixfs/importer/helpers"
59
"github.com/ipfs/boxo/ipld/unixfs/io"
10+
"github.com/ipfs/boxo/verifcid"
11+
mh "github.com/multiformats/go-multihash"
612
)
713

814
const (
@@ -43,3 +49,132 @@ type Import struct {
4349
BatchMaxNodes OptionalInteger
4450
BatchMaxSize OptionalInteger
4551
}
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+
}

0 commit comments

Comments
 (0)