Skip to content

Commit 4468527

Browse files
committed
fix(config): harden provide strategy parsing with error returns
- config: ParseProvideStrategy returns error, rejects "all" mixed with selective strategies, removes dead strategy==0 check - config: add MustParseProvideStrategy for pre-validated call sites - config: ValidateProvideConfig validates strategy at startup - config: ShouldProvideForStrategy uses bitmask check for ProvideStrategyAll - core/node: downstream callers use MustParseProvideStrategy - core/node: fix Pinning() nil return that caused fx.Provide panic
1 parent 6f209df commit 4468527

7 files changed

Lines changed: 154 additions & 36 deletions

File tree

config/provide.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,25 +100,55 @@ type ProvideDHT struct {
100100
ResumeEnabled Flag `json:",omitempty"`
101101
}
102102

103-
func ParseProvideStrategy(s string) ProvideStrategy {
103+
func ParseProvideStrategy(s string) (ProvideStrategy, error) {
104104
var strategy ProvideStrategy
105105
for part := range strings.SplitSeq(s, "+") {
106106
switch part {
107-
case "all", "flat", "": // special case, does not mix with others ("flat" is deprecated, maps to "all")
108-
return ProvideStrategyAll
107+
case "all", "flat":
108+
strategy |= ProvideStrategyAll
109+
case "":
110+
// empty string (default config) maps to "all",
111+
// but empty tokens from splitting (e.g. "pinned+") are invalid
112+
if s == "" {
113+
strategy |= ProvideStrategyAll
114+
} else {
115+
return 0, fmt.Errorf("invalid provide strategy: empty token in %q", s)
116+
}
109117
case "pinned":
110118
strategy |= ProvideStrategyPinned
111119
case "roots":
112120
strategy |= ProvideStrategyRoots
113121
case "mfs":
114122
strategy |= ProvideStrategyMFS
123+
default:
124+
return 0, fmt.Errorf("unknown provide strategy token: %q in %q", part, s)
115125
}
116126
}
127+
// "all" provides every block and cannot be combined with selective strategies
128+
if strategy&ProvideStrategyAll != 0 && strategy != ProvideStrategyAll {
129+
return 0, fmt.Errorf("\"all\" strategy cannot be combined with other strategies in %q", s)
130+
}
131+
return strategy, nil
132+
}
133+
134+
// MustParseProvideStrategy is like ParseProvideStrategy but panics on error.
135+
// Use with strategy strings that have already been validated at startup.
136+
func MustParseProvideStrategy(s string) ProvideStrategy {
137+
strategy, err := ParseProvideStrategy(s)
138+
if err != nil {
139+
panic(err)
140+
}
117141
return strategy
118142
}
119143

120144
// ValidateProvideConfig validates the Provide configuration according to DHT requirements.
121145
func ValidateProvideConfig(cfg *Provide) error {
146+
// Validate Provide.Strategy
147+
strategy := cfg.Strategy.WithDefault(DefaultProvideStrategy)
148+
if _, err := ParseProvideStrategy(strategy); err != nil {
149+
return fmt.Errorf("Provide.Strategy: %w", err)
150+
}
151+
122152
// Validate Provide.DHT.Interval
123153
if !cfg.DHT.Interval.IsDefault() {
124154
interval := cfg.DHT.Interval.WithDefault(DefaultProvideDHTInterval)
@@ -184,7 +214,7 @@ func ValidateProvideConfig(cfg *Provide) error {
184214
// ShouldProvideForStrategy determines if content should be provided based on the provide strategy
185215
// and content characteristics (pinned status, root status, MFS status).
186216
func ShouldProvideForStrategy(strategy ProvideStrategy, isPinned bool, isPinnedRoot bool, isMFS bool) bool {
187-
if strategy == ProvideStrategyAll {
217+
if strategy&ProvideStrategyAll != 0 {
188218
// 'all' strategy: always provide
189219
return true
190220
}

config/provide_test.go

Lines changed: 111 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,119 @@ import (
99
)
1010

1111
func TestParseProvideStrategy(t *testing.T) {
12-
tests := []struct {
13-
input string
14-
expect ProvideStrategy
15-
}{
16-
{"all", ProvideStrategyAll},
17-
{"pinned", ProvideStrategyPinned},
18-
{"mfs", ProvideStrategyMFS},
19-
{"pinned+mfs", ProvideStrategyPinned | ProvideStrategyMFS},
20-
{"invalid", 0},
21-
{"all+invalid", ProvideStrategyAll},
22-
{"", ProvideStrategyAll},
23-
{"flat", ProvideStrategyAll}, // deprecated, maps to "all"
24-
{"flat+all", ProvideStrategyAll},
25-
}
12+
t.Run("valid strategies", func(t *testing.T) {
13+
tests := []struct {
14+
input string
15+
expect ProvideStrategy
16+
}{
17+
{"all", ProvideStrategyAll},
18+
{"pinned", ProvideStrategyPinned},
19+
{"roots", ProvideStrategyRoots},
20+
{"mfs", ProvideStrategyMFS},
21+
{"pinned+mfs", ProvideStrategyPinned | ProvideStrategyMFS},
22+
{"pinned+roots", ProvideStrategyPinned | ProvideStrategyRoots},
23+
{"pinned+mfs+roots", ProvideStrategyPinned | ProvideStrategyMFS | ProvideStrategyRoots},
24+
{"", ProvideStrategyAll}, // empty string = default = all
25+
{"flat", ProvideStrategyAll}, // deprecated, maps to "all"
26+
{"flat+all", ProvideStrategyAll}, // redundant but valid
27+
{"all+all", ProvideStrategyAll}, // redundant but valid
28+
{"mfs+pinned", ProvideStrategyMFS | ProvideStrategyPinned}, // order doesn't matter
29+
}
2630

27-
for _, tt := range tests {
28-
result := ParseProvideStrategy(tt.input)
29-
if result != tt.expect {
30-
t.Errorf("ParseProvideStrategy(%q) = %d, want %d", tt.input, result, tt.expect)
31+
for _, tt := range tests {
32+
result, err := ParseProvideStrategy(tt.input)
33+
require.NoError(t, err, "ParseProvideStrategy(%q)", tt.input)
34+
assert.Equal(t, tt.expect, result, "ParseProvideStrategy(%q)", tt.input)
3135
}
32-
}
36+
})
37+
38+
t.Run("unknown token (including typos)", func(t *testing.T) {
39+
tests := []struct {
40+
input string
41+
err string
42+
}{
43+
{"invalid", `unknown provide strategy token: "invalid"`},
44+
{"uniuqe", `unknown provide strategy token: "uniuqe"`}, // typo of "unique"
45+
{"entites", `unknown provide strategy token: "entites"`}, // cspell:disable-line -- intentional typo of "entities"
46+
{"pinned+uniuqe", `unknown provide strategy token: "uniuqe"`}, // typo in combo
47+
}
48+
49+
for _, tt := range tests {
50+
_, err := ParseProvideStrategy(tt.input)
51+
require.Error(t, err, "ParseProvideStrategy(%q) should fail", tt.input)
52+
assert.Contains(t, err.Error(), tt.err)
53+
}
54+
})
55+
56+
t.Run("empty token from delimiter", func(t *testing.T) {
57+
tests := []string{
58+
"pinned+", // trailing +
59+
"+pinned", // leading +
60+
"pinned++mfs", // double +
61+
}
62+
63+
for _, input := range tests {
64+
_, err := ParseProvideStrategy(input)
65+
require.Error(t, err, "ParseProvideStrategy(%q) should fail", input)
66+
assert.Contains(t, err.Error(), "empty token")
67+
}
68+
})
69+
70+
t.Run("all cannot be combined with other strategies", func(t *testing.T) {
71+
tests := []string{
72+
"all+pinned",
73+
"all+mfs",
74+
"all+roots",
75+
"flat+pinned",
76+
"all+pinned+mfs",
77+
}
78+
79+
for _, input := range tests {
80+
_, err := ParseProvideStrategy(input)
81+
require.Error(t, err, "ParseProvideStrategy(%q) should fail", input)
82+
assert.Contains(t, err.Error(), "cannot be combined")
83+
}
84+
})
85+
}
86+
87+
func TestMustParseProvideStrategy(t *testing.T) {
88+
t.Run("valid input returns strategy", func(t *testing.T) {
89+
assert.Equal(t, ProvideStrategyAll, MustParseProvideStrategy("all"))
90+
assert.Equal(t, ProvideStrategyPinned|ProvideStrategyMFS, MustParseProvideStrategy("pinned+mfs"))
91+
})
92+
93+
t.Run("invalid input panics", func(t *testing.T) {
94+
assert.Panics(t, func() { MustParseProvideStrategy("bogus") })
95+
assert.Panics(t, func() { MustParseProvideStrategy("all+pinned") })
96+
})
97+
}
98+
99+
func TestValidateProvideConfig_Strategy(t *testing.T) {
100+
t.Run("valid strategies", func(t *testing.T) {
101+
for _, s := range []string{"all", "pinned", "roots", "mfs", "pinned+mfs"} {
102+
cfg := &Provide{Strategy: NewOptionalString(s)}
103+
require.NoError(t, ValidateProvideConfig(cfg), "strategy=%q", s)
104+
}
105+
})
106+
107+
t.Run("default (nil) strategy is valid", func(t *testing.T) {
108+
cfg := &Provide{}
109+
require.NoError(t, ValidateProvideConfig(cfg))
110+
})
111+
112+
t.Run("invalid strategy", func(t *testing.T) {
113+
cfg := &Provide{Strategy: NewOptionalString("bogus")}
114+
err := ValidateProvideConfig(cfg)
115+
require.Error(t, err)
116+
assert.Contains(t, err.Error(), "Provide.Strategy")
117+
})
118+
119+
t.Run("all combined with others", func(t *testing.T) {
120+
cfg := &Provide{Strategy: NewOptionalString("all+pinned")}
121+
err := ValidateProvideConfig(cfg)
122+
require.Error(t, err)
123+
assert.Contains(t, err.Error(), "cannot be combined")
124+
})
33125
}
34126

35127
func TestValidateProvideConfig_Interval(t *testing.T) {

core/commands/cmdenv/env.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func ExecuteFastProvide(
156156

157157
// Check if strategy allows providing this content
158158
strategyStr := cfg.Provide.Strategy.WithDefault(config.DefaultProvideStrategy)
159-
strategy := config.ParseProvideStrategy(strategyStr)
159+
strategy := config.MustParseProvideStrategy(strategyStr)
160160
shouldProvide := config.ShouldProvideForStrategy(strategy, isPinned, isPinnedRoot, isMFS)
161161

162162
if !shouldProvide {

core/node/core.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,7 @@ func BlockService(cfg *config.Config) func(lc fx.Lifecycle, bs blockstore.Blocks
5252

5353
// Pinning creates new pinner which tells GC which blocks should be kept
5454
func Pinning(strategy string) func(bstore blockstore.Blockstore, ds format.DAGService, repo repo.Repo, prov DHTProvider) (pin.Pinner, error) {
55-
// Parse strategy at function creation time (not inside the returned function)
56-
// This happens before the provider is created, which is why we pass the strategy
57-
// string and parse it here, rather than using fx-provided ProvidingStrategy.
58-
strategyFlag := config.ParseProvideStrategy(strategy)
55+
strategyFlag := config.MustParseProvideStrategy(strategy)
5956

6057
return func(bstore blockstore.Blockstore,
6158
ds format.DAGService,
@@ -238,7 +235,7 @@ func Files(strategy string) func(mctx helpers.MetricsCtx, lc fx.Lifecycle, repo
238235
// strategy - it ensures all MFS content gets announced as it's added or
239236
// modified. For non-mfs strategies, we set provider to nil to avoid
240237
// unnecessary providing.
241-
strategyFlag := config.ParseProvideStrategy(strategy)
238+
strategyFlag := config.MustParseProvideStrategy(strategy)
242239
if strategyFlag&config.ProvideStrategyMFS == 0 {
243240
prov = nil
244241
}

core/node/provider.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,9 +1094,8 @@ func OnlineProviders(provide bool, cfg *config.Config) fx.Option {
10941094

10951095
providerStrategy := cfg.Provide.Strategy.WithDefault(config.DefaultProvideStrategy)
10961096

1097-
strategyFlag := config.ParseProvideStrategy(providerStrategy)
1098-
if strategyFlag == 0 {
1099-
return fx.Error(fmt.Errorf("provider: unknown strategy %q", providerStrategy))
1097+
if _, err := config.ParseProvideStrategy(providerStrategy); err != nil {
1098+
return fx.Error(fmt.Errorf("provider: %w", err))
11001099
}
11011100

11021101
opts := []fx.Option{
@@ -1240,7 +1239,7 @@ func handleStrategyChange(strategy string, provider DHTProvider, ds datastore.Da
12401239
}
12411240

12421241
func setReproviderKeyProvider(strategy string) func(in provStrategyIn) provStrategyOut {
1243-
strategyFlag := config.ParseProvideStrategy(strategy)
1242+
strategyFlag := config.MustParseProvideStrategy(strategy)
12441243

12451244
return func(in provStrategyIn) provStrategyOut {
12461245
// Create the appropriate key provider based on strategy

core/node/storage.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func BaseBlockstoreCtor(
4141
// Important: Provide calls from blockstore are intentionally BLOCKING.
4242
// The Provider implementation (not the blockstore) should handle concurrency/queuing.
4343
// This avoids spawning unbounded goroutines for concurrent block additions.
44-
strategyFlag := config.ParseProvideStrategy(providingStrategy)
44+
strategyFlag := config.MustParseProvideStrategy(providingStrategy)
4545
if strategyFlag&config.ProvideStrategyAll != 0 {
4646
opts = append(opts, blockstore.Provider(prov))
4747
}

test/cli/migrations/migration_17_to_latest_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,8 @@ func testInvalidStrategyMigration(t *testing.T) {
213213
outputStr := string(output)
214214
t.Logf("Daemon output with invalid strategy: %s", outputStr)
215215

216-
// The error should mention unknown strategy
217-
require.Contains(t, outputStr, "unknown strategy", "Should report unknown strategy error")
216+
// The error should mention unknown strategy token
217+
require.Contains(t, outputStr, "unknown provide strategy token", "Should report unknown strategy error")
218218
}
219219

220220
func testRepoProviderReproviderMigration(t *testing.T) {

0 commit comments

Comments
 (0)