Skip to content

Commit ab4ca67

Browse files
committed
feat: limit pin names to 255 bytes
adds validation to ensure pin names don't exceed 255 bytes across all commands that accept pin names. this prevents issues with filesystem limitations and improves compatibility. affected commands: - ipfs pin add --name - ipfs add --pin-name - ipfs pin ls --name (filter) - ipfs pin remote add --name - ipfs pin remote ls --name (filter) - ipfs pin remote rm --name (filter)
1 parent 22f0377 commit ab4ca67

File tree

6 files changed

+229
-1
lines changed

6 files changed

+229
-1
lines changed

core/commands/add.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/ipfs/kubo/config"
1313
"github.com/ipfs/kubo/core/commands/cmdenv"
14+
"github.com/ipfs/kubo/core/commands/cmdutils"
1415

1516
"github.com/cheggaaa/pb"
1617
"github.com/ipfs/boxo/files"
@@ -269,6 +270,13 @@ https://github.com/ipfs/kubo/blob/master/docs/config.md#import
269270
return fmt.Errorf("inline-limit %d exceeds maximum allowed size of %d bytes", inlineLimit, verifcid.DefaultMaxIdentityDigestSize)
270271
}
271272

273+
// Validate pin name
274+
if pinNameSet {
275+
if err := cmdutils.ValidatePinName(pinName); err != nil {
276+
return err
277+
}
278+
}
279+
272280
toFilesStr, toFilesSet := req.Options[toFilesOptionName].(string)
273281
preserveMode, _ := req.Options[preserveModeOptionName].(bool)
274282
preserveMtime, _ := req.Options[preserveMtimeOptionName].(bool)

core/commands/cmdutils/utils.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
const (
1414
AllowBigBlockOptionName = "allow-big-block"
1515
SoftBlockLimit = 1024 * 1024 // https://github.com/ipfs/kubo/issues/7421#issuecomment-910833499
16+
MaxPinNameBytes = 255 // Maximum number of bytes allowed for a pin name
1617
)
1718

1819
var AllowBigBlockOption cmds.Option
@@ -50,6 +51,21 @@ func CheckBlockSize(req *cmds.Request, size uint64) error {
5051
return nil
5152
}
5253

54+
// ValidatePinName validates that a pin name does not exceed the maximum allowed byte length.
55+
// Returns an error if the name exceeds MaxPinNameBytes (255 bytes).
56+
func ValidatePinName(name string) error {
57+
if name == "" {
58+
// Empty names are allowed
59+
return nil
60+
}
61+
62+
nameBytes := len([]byte(name))
63+
if nameBytes > MaxPinNameBytes {
64+
return fmt.Errorf("pin name is %d bytes (max %d bytes)", nameBytes, MaxPinNameBytes)
65+
}
66+
return nil
67+
}
68+
5369
// PathOrCidPath returns a path.Path built from the argument. It keeps the old
5470
// behaviour by building a path from a CID string.
5571
func PathOrCidPath(str string) (path.Path, error) {

core/commands/pin/pin.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ It may take some time. Pass '--progress' to track the progress.
100100
name, _ := req.Options[pinNameOptionName].(string)
101101
showProgress, _ := req.Options[pinProgressOptionName].(bool)
102102

103+
// Validate pin name
104+
if err := cmdutils.ValidatePinName(name); err != nil {
105+
return err
106+
}
107+
103108
if err := req.ParseBodyArgs(); err != nil {
104109
return err
105110
}
@@ -385,6 +390,11 @@ Example:
385390
displayNames, _ := req.Options[pinNamesOptionName].(bool)
386391
name, _ := req.Options[pinNameOptionName].(string)
387392

393+
// Validate name filter
394+
if err := cmdutils.ValidatePinName(name); err != nil {
395+
return err
396+
}
397+
388398
mode, ok := pin.StringToMode(typeStr)
389399
if !ok {
390400
return fmt.Errorf("invalid type '%s', must be one of {direct, indirect, recursive, all}", typeStr)

core/commands/pin/remotepin.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ NOTE: a comma-separated notation is supported in CLI for convenience:
171171
opts := []pinclient.AddOption{}
172172
if name, nameFound := req.Options[pinNameOptionName]; nameFound {
173173
nameStr := name.(string)
174+
// Validate pin name
175+
if err := cmdutils.ValidatePinName(nameStr); err != nil {
176+
return err
177+
}
174178
opts = append(opts, pinclient.PinOpts.WithName(nameStr))
175179
}
176180

@@ -321,6 +325,11 @@ func lsRemote(ctx context.Context, req *cmds.Request, c *pinclient.Client, out c
321325
opts := []pinclient.LsOption{}
322326
if name, nameFound := req.Options[pinNameOptionName]; nameFound {
323327
nameStr := name.(string)
328+
// Validate name filter
329+
if err := cmdutils.ValidatePinName(nameStr); err != nil {
330+
close(out)
331+
return err
332+
}
324333
opts = append(opts, pinclient.PinOpts.FilterName(nameStr))
325334
}
326335

docs/changelogs/v0.38.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
1515
- [📊 Exposed DHT metrics](#-exposed-dht-metrics)
1616
- [🚨 Improved gateway error pages with diagnostic tools](#-improved-gateway-error-pages-with-diagnostic-tools)
1717
- [🎨 Updated WebUI](#-updated-webui)
18+
- [📌 Pin name improvements](#-pin-name-improvements)
1819
- [🛠️ Identity CID size enforcement and `ipfs files write` fixes](#️-identity-cid-size-enforcement-and-ipfs-files-write-fixes)
1920
- [📦️ Important dependency updates](#-important-dependency-updates)
2021
- [📝 Changelog](#-changelog)
@@ -91,7 +92,7 @@ Additional improvements include a close button in the file viewer, better error
9192

9293
#### 📌 Pin name improvements
9394

94-
`ipfs pin ls <cid> --names` now correctly returns pin names for specific CIDs ([#10649](https://github.com/ipfs/kubo/issues/10649), [boxo#1035](https://github.com/ipfs/boxo/pull/1035)), and RPC no longer incorrectly returns names from other pins ([#10966](https://github.com/ipfs/kubo/pull/10966)).
95+
`ipfs pin ls <cid> --names` now correctly returns pin names for specific CIDs ([#10649](https://github.com/ipfs/kubo/issues/10649), [boxo#1035](https://github.com/ipfs/boxo/pull/1035)), RPC no longer incorrectly returns names from other pins ([#10966](https://github.com/ipfs/kubo/pull/10966)), and pin names are now limited to 255 bytes for better cross-platform compatibility ([#10981](https://github.com/ipfs/kubo/pull/10981)).
9596

9697
#### 🛠️ Identity CID size enforcement and `ipfs files write` fixes
9798

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"testing"
7+
8+
"github.com/ipfs/kubo/test/cli/harness"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestPinNameValidation(t *testing.T) {
13+
t.Parallel()
14+
15+
// Create a test node and add a test file
16+
node := harness.NewT(t).NewNode().Init().StartDaemon("--offline")
17+
defer node.StopDaemon()
18+
19+
// Add a test file to get a CID
20+
testContent := "test content for pin name validation"
21+
testCID := node.IPFSAddStr(testContent, "--pin=false")
22+
23+
t.Run("pin add accepts valid names", func(t *testing.T) {
24+
testCases := []struct {
25+
name string
26+
pinName string
27+
description string
28+
}{
29+
{
30+
name: "empty_name",
31+
pinName: "",
32+
description: "Empty name should be allowed",
33+
},
34+
{
35+
name: "short_name",
36+
pinName: "test",
37+
description: "Short ASCII name should be allowed",
38+
},
39+
{
40+
name: "max_255_bytes",
41+
pinName: strings.Repeat("a", 255),
42+
description: "Exactly 255 bytes should be allowed",
43+
},
44+
{
45+
name: "unicode_within_limit",
46+
pinName: "测试名称🔥", // Chinese characters and emoji
47+
description: "Unicode characters within 255 bytes should be allowed",
48+
},
49+
}
50+
51+
for _, tc := range testCases {
52+
t.Run(tc.name, func(t *testing.T) {
53+
var args []string
54+
if tc.pinName != "" {
55+
args = []string{"pin", "add", "--name", tc.pinName, testCID}
56+
} else {
57+
args = []string{"pin", "add", testCID}
58+
}
59+
60+
res := node.RunIPFS(args...)
61+
require.Equal(t, 0, res.ExitCode(), tc.description)
62+
63+
// Clean up - unpin
64+
node.RunIPFS("pin", "rm", testCID)
65+
})
66+
}
67+
})
68+
69+
t.Run("pin add rejects names exceeding 255 bytes", func(t *testing.T) {
70+
testCases := []struct {
71+
name string
72+
pinName string
73+
description string
74+
}{
75+
{
76+
name: "256_bytes",
77+
pinName: strings.Repeat("a", 256),
78+
description: "256 bytes should be rejected",
79+
},
80+
{
81+
name: "300_bytes",
82+
pinName: strings.Repeat("b", 300),
83+
description: "300 bytes should be rejected",
84+
},
85+
{
86+
name: "unicode_exceeding_limit",
87+
pinName: strings.Repeat("测", 100), // Each Chinese character is 3 bytes, total 300 bytes
88+
description: "Unicode string exceeding 255 bytes should be rejected",
89+
},
90+
}
91+
92+
for _, tc := range testCases {
93+
t.Run(tc.name, func(t *testing.T) {
94+
res := node.RunIPFS("pin", "add", "--name", tc.pinName, testCID)
95+
require.NotEqual(t, 0, res.ExitCode(), tc.description)
96+
require.Contains(t, res.Stderr.String(), "max 255 bytes", "Error should mention the 255 byte limit")
97+
})
98+
}
99+
})
100+
101+
t.Run("pin ls with name filter validates length", func(t *testing.T) {
102+
// Test valid filter
103+
res := node.RunIPFS("pin", "ls", "--name", strings.Repeat("a", 255))
104+
require.Equal(t, 0, res.ExitCode(), "255-byte name filter should be accepted")
105+
106+
// Test invalid filter
107+
res = node.RunIPFS("pin", "ls", "--name", strings.Repeat("a", 256))
108+
require.NotEqual(t, 0, res.ExitCode(), "256-byte name filter should be rejected")
109+
require.Contains(t, res.Stderr.String(), "max 255 bytes", "Error should mention the 255 byte limit")
110+
})
111+
}
112+
113+
func TestAddPinNameValidation(t *testing.T) {
114+
t.Parallel()
115+
116+
node := harness.NewT(t).NewNode().Init().StartDaemon("--offline")
117+
defer node.StopDaemon()
118+
119+
// Create a test file
120+
testFile := "test.txt"
121+
node.WriteBytes(testFile, []byte("test content for add command"))
122+
123+
t.Run("ipfs add with --pin-name accepts valid names", func(t *testing.T) {
124+
testCases := []struct {
125+
name string
126+
pinName string
127+
description string
128+
}{
129+
{
130+
name: "short_name",
131+
pinName: "test-add",
132+
description: "Short ASCII name should be allowed",
133+
},
134+
{
135+
name: "max_255_bytes",
136+
pinName: strings.Repeat("x", 255),
137+
description: "Exactly 255 bytes should be allowed",
138+
},
139+
}
140+
141+
for _, tc := range testCases {
142+
t.Run(tc.name, func(t *testing.T) {
143+
res := node.RunIPFS("add", fmt.Sprintf("--pin-name=%s", tc.pinName), "-q", testFile)
144+
require.Equal(t, 0, res.ExitCode(), tc.description)
145+
cid := strings.TrimSpace(res.Stdout.String())
146+
147+
// Verify pin exists with name
148+
lsRes := node.RunIPFS("pin", "ls", "--names", "--type=recursive", cid)
149+
require.Equal(t, 0, lsRes.ExitCode())
150+
require.Contains(t, lsRes.Stdout.String(), tc.pinName, "Pin should have the specified name")
151+
152+
// Clean up
153+
node.RunIPFS("pin", "rm", cid)
154+
})
155+
}
156+
})
157+
158+
t.Run("ipfs add with --pin-name rejects names exceeding 255 bytes", func(t *testing.T) {
159+
testCases := []struct {
160+
name string
161+
pinName string
162+
description string
163+
}{
164+
{
165+
name: "256_bytes",
166+
pinName: strings.Repeat("y", 256),
167+
description: "256 bytes should be rejected",
168+
},
169+
{
170+
name: "500_bytes",
171+
pinName: strings.Repeat("z", 500),
172+
description: "500 bytes should be rejected",
173+
},
174+
}
175+
176+
for _, tc := range testCases {
177+
t.Run(tc.name, func(t *testing.T) {
178+
res := node.RunIPFS("add", fmt.Sprintf("--pin-name=%s", tc.pinName), testFile)
179+
require.NotEqual(t, 0, res.ExitCode(), tc.description)
180+
require.Contains(t, res.Stderr.String(), "max 255 bytes", "Error should mention the 255 byte limit")
181+
})
182+
}
183+
})
184+
}

0 commit comments

Comments
 (0)