Skip to content

Commit 4b70e02

Browse files
authored
[breaking] fix: validate sketch name (#2051)
1 parent cddbff3 commit 4b70e02

File tree

5 files changed

+151
-7
lines changed

5 files changed

+151
-7
lines changed

commands/sketch/new.go

+27
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package sketch
1818
import (
1919
"context"
2020
"errors"
21+
"regexp"
2122

2223
"github.com/arduino/arduino-cli/arduino"
2324
"github.com/arduino/arduino-cli/arduino/globals"
@@ -34,6 +35,10 @@ void loop() {
3435
}
3536
`)
3637

38+
// sketchNameMaxLength could be part of the regex, but it's intentionally left out for clearer error reporting
39+
var sketchNameMaxLength = 63
40+
var sketchNameValidationRegex = regexp.MustCompile(`^[0-9a-zA-Z][0-9a-zA-Z_\.-]*$`)
41+
3742
// NewSketch creates a new sketch via gRPC
3843
func NewSketch(ctx context.Context, req *rpc.NewSketchRequest) (*rpc.NewSketchResponse, error) {
3944
var sketchesDir string
@@ -42,6 +47,11 @@ func NewSketch(ctx context.Context, req *rpc.NewSketchRequest) (*rpc.NewSketchRe
4247
} else {
4348
sketchesDir = configuration.Settings.GetString("directories.User")
4449
}
50+
51+
if err := validateSketchName(req.SketchName); err != nil {
52+
return nil, err
53+
}
54+
4555
sketchDirPath := paths.New(sketchesDir).Join(req.SketchName)
4656
if err := sketchDirPath.MkdirAll(); err != nil {
4757
return nil, &arduino.CantCreateSketchError{Cause: err}
@@ -59,3 +69,20 @@ func NewSketch(ctx context.Context, req *rpc.NewSketchRequest) (*rpc.NewSketchRe
5969

6070
return &rpc.NewSketchResponse{MainFile: sketchMainFilePath.String()}, nil
6171
}
72+
73+
func validateSketchName(name string) error {
74+
if name == "" {
75+
return &arduino.CantCreateSketchError{Cause: errors.New(tr("sketch name cannot be empty"))}
76+
}
77+
if len(name) > sketchNameMaxLength {
78+
return &arduino.CantCreateSketchError{Cause: errors.New(tr("sketch name too long (%d characters). Maximum allowed length is %d",
79+
len(name),
80+
sketchNameMaxLength))}
81+
}
82+
if !sketchNameValidationRegex.MatchString(name) {
83+
return &arduino.CantCreateSketchError{Cause: errors.New(tr("invalid sketch name \"%s\". Required pattern %s",
84+
name,
85+
sketchNameValidationRegex.String()))}
86+
}
87+
return nil
88+
}

commands/sketch/new_test.go

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package sketch
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func Test_SketchNameWrongPattern(t *testing.T) {
12+
invalidNames := []string{
13+
"&",
14+
"",
15+
".hello",
16+
"_hello",
17+
"-hello",
18+
"hello*",
19+
"||||||||||||||",
20+
",`hack[}attempt{];",
21+
}
22+
for _, name := range invalidNames {
23+
_, err := NewSketch(context.Background(), &commands.NewSketchRequest{
24+
SketchName: name,
25+
SketchDir: t.TempDir(),
26+
})
27+
require.NotNil(t, err)
28+
29+
require.Error(t, err, `Can't create sketch: invalid sketch name "%s". Required pattern %s`,
30+
name,
31+
sketchNameValidationRegex)
32+
}
33+
}
34+
35+
func Test_SketchNameEmpty(t *testing.T) {
36+
emptyName := ""
37+
_, err := NewSketch(context.Background(), &commands.NewSketchRequest{
38+
SketchName: emptyName,
39+
SketchDir: t.TempDir(),
40+
})
41+
require.NotNil(t, err)
42+
43+
require.Error(t, err, `Can't create sketch: sketch name cannot be empty`)
44+
}
45+
46+
func Test_SketchNameTooLong(t *testing.T) {
47+
tooLongName := make([]byte, sketchNameMaxLength+1)
48+
for i := range tooLongName {
49+
tooLongName[i] = 'a'
50+
}
51+
_, err := NewSketch(context.Background(), &commands.NewSketchRequest{
52+
SketchName: string(tooLongName),
53+
SketchDir: t.TempDir(),
54+
})
55+
require.NotNil(t, err)
56+
57+
require.Error(t, err, `Can't create sketch: sketch name too long (%d characters). Maximum allowed length is %d`,
58+
len(tooLongName),
59+
sketchNameMaxLength)
60+
}
61+
62+
func Test_SketchNameOk(t *testing.T) {
63+
lengthLimitName := make([]byte, sketchNameMaxLength)
64+
for i := range lengthLimitName {
65+
lengthLimitName[i] = 'a'
66+
}
67+
validNames := []string{
68+
"h",
69+
"h.ello",
70+
"h..ello-world",
71+
"h..ello-world.",
72+
"hello_world__",
73+
string(lengthLimitName),
74+
}
75+
for _, name := range validNames {
76+
_, err := NewSketch(context.Background(), &commands.NewSketchRequest{
77+
SketchName: name,
78+
SketchDir: t.TempDir(),
79+
})
80+
require.Nil(t, err)
81+
}
82+
}

docs/UPGRADING.md

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ Here you can find a list of migration guides to handle breaking changes between
44

55
## 0.30.0
66

7+
### Sketch name validation
8+
9+
The sketch name submitted via the `sketch new` command of the CLI or the gRPC command
10+
`cc.arduino.cli.commands.v1.NewSketch` are now validated. The applied rules follow the
11+
[sketch specifications](https://arduino.github.io/arduino-cli/dev/sketch-specification).
12+
13+
Existing sketch names violating the new constraint need to be updated.
14+
715
### `daemon` CLI command's `--ip` flag removal
816

917
The `daemon` CLI command no longer allows to set a custom ip for the gRPC communication. Currently there is not enough

internal/cli/sketch/new.go

+24-7
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,33 @@ func initNewCommand() *cobra.Command {
4949
func runNewCommand(args []string, overwrite bool) {
5050
logrus.Info("Executing `arduino-cli sketch new`")
5151
// Trim to avoid issues if user creates a sketch adding the .ino extesion to the name
52-
sketchName := args[0]
53-
trimmedSketchName := strings.TrimSuffix(sketchName, globals.MainFileValidExtension)
54-
sketchDirPath, err := paths.New(trimmedSketchName).Abs()
55-
if err != nil {
56-
feedback.Fatal(tr("Error creating sketch: %v", err), feedback.ErrGeneric)
52+
inputSketchName := args[0]
53+
trimmedSketchName := strings.TrimSuffix(inputSketchName, globals.MainFileValidExtension)
54+
55+
var sketchDir string
56+
var sketchName string
57+
var sketchDirPath *paths.Path
58+
var err error
59+
60+
if trimmedSketchName == "" {
61+
// `paths.New` returns nil with an empty string so `paths.Abs` panics.
62+
// if the name is empty we rely on the "new" command to fail nicely later
63+
// with the same logic in grpc and cli flows
64+
sketchDir = ""
65+
sketchName = ""
66+
} else {
67+
sketchDirPath, err = paths.New(trimmedSketchName).Abs()
68+
if err != nil {
69+
feedback.Fatal(tr("Error creating sketch: %v", err), feedback.ErrGeneric)
70+
}
71+
sketchDir = sketchDirPath.Parent().String()
72+
sketchName = sketchDirPath.Base()
5773
}
74+
5875
_, err = sk.NewSketch(context.Background(), &rpc.NewSketchRequest{
5976
Instance: nil,
60-
SketchName: sketchDirPath.Base(),
61-
SketchDir: sketchDirPath.Parent().String(),
77+
SketchName: sketchName,
78+
SketchDir: sketchDir,
6279
Overwrite: overwrite,
6380
})
6481
if err != nil {

internal/integrationtest/sketch/sketch_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ func TestSketchNew(t *testing.T) {
7373
require.FileExists(t, currentSketchPath.Join(sketchName).String()+".ino")
7474
}
7575

76+
func TestSketchNewEmptyName(t *testing.T) {
77+
// testing that we fail nicely. It panicked in the past
78+
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
79+
defer env.CleanUp()
80+
81+
sketchName := ""
82+
_, _, err := cli.Run("sketch", "new", sketchName)
83+
require.Error(t, err, "Can't create sketch: sketch name cannot be empty")
84+
}
85+
7686
func verifyZipContainsSketchExcludingBuildDir(t *testing.T, files []*zip.File) {
7787
require.Len(t, files, 8)
7888
require.Equal(t, paths.New("sketch_simple", "doc.txt").String(), files[0].Name)

0 commit comments

Comments
 (0)