Skip to content

Commit

Permalink
Escape config file name in schedule parameters (#420)
Browse files Browse the repository at this point in the history
* update test with escaped command

* escape restic binary if containing spaces

* add quotes around config file in scheduling

* fix elevated command line with a config file with spaces

* add build tag for windows only
  • Loading branch information
creativeprojects authored Oct 18, 2024
1 parent 62f3118 commit fb6c7a2
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 11 deletions.
2 changes: 1 addition & 1 deletion schedule_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func scheduleJobs(handler schedule.Handler, profileName string, configs []*confi
args := []string{
"--no-ansi",
"--config",
scheduleConfig.ConfigFile,
`"` + scheduleConfig.ConfigFile + `"`,
"run-schedule",
scheduleName,
}
Expand Down
3 changes: 2 additions & 1 deletion schedule_jobs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ func TestSimpleScheduleJob(t *testing.T) {
mock.AnythingOfType("[]*calendar.Event"),
mock.AnythingOfType("string")).
RunAndReturn(func(scheduleConfig *schedule.Config, events []*calendar.Event, permission string) error {
assert.Equal(t, []string{"--no-ansi", "--config", "", "run-schedule", "backup@profile"}, scheduleConfig.Arguments)
assert.Equal(t, []string{"--no-ansi", "--config", `"config.file"`, "run-schedule", "backup@profile"}, scheduleConfig.Arguments)
return nil
})

scheduleConfig := configForJob("backup", "sched")
scheduleConfig.ConfigFile = "config.file"
err := scheduleJobs(handler, "profile", []*config.Schedule{scheduleConfig})
assert.NoError(t, err)
}
Expand Down
3 changes: 2 additions & 1 deletion shell/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ func (c *Command) Run() (monitor.Summary, string, error) {
return summary, errorText, err
}

// GetShellCommand transforms the command line and arguments to be launched via a shell (sh or cmd.exe)
// GetShellCommand transforms the command line and arguments to be launched via a shell (sh or cmd.exe).
// This method doesn't escape any argument containing spaces, it should have been dealt with before.
func (c *Command) GetShellCommand() (shell string, arguments []string, err error) {
var searchList []string
for _, sh := range c.Shell {
Expand Down
12 changes: 6 additions & 6 deletions shell/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func TestRemoveQuotes(t *testing.T) {
func TestShellCommandWithArguments(t *testing.T) {
t.Parallel()

testCommand := "/bin/restic"
testCommand := `"/bin/with space/restic"`
testArgs := []string{
`-v`,
`--exclude-file`,
Expand All @@ -82,7 +82,7 @@ func TestShellCommandWithArguments(t *testing.T) {
assert.Equal(t, `c:\windows\system32\cmd.exe`, strings.ToLower(command))
assert.Equal(t, []string{
`/V:ON`, `/C`,
`/bin/restic`,
`"/bin/with space/restic"`,
`-v`,
`--exclude-file`,
`excludes`,
Expand All @@ -95,15 +95,15 @@ func TestShellCommandWithArguments(t *testing.T) {
assert.Regexp(t, regexp.MustCompile("(/usr)?/bin/(ba)?sh"), command)
assert.Equal(t, []string{
"-c",
"/bin/restic -v --exclude-file \"excludes\" --repo \"/path/with space\" backup .",
"\"/bin/with space/restic\" -v --exclude-file \"excludes\" --repo \"/path/with space\" backup .",
}, args)
}
}

func TestShellCommand(t *testing.T) {
t.Parallel()

testCommand := "/bin/restic -v --exclude-file \"excludes\" --repo \"/path/with space\" backup ."
testCommand := "\"/bin/with space/restic\" -v --exclude-file \"excludes\" --repo \"/path/with space\" backup ."
testArgs := []string{}
c := &Command{
Command: testCommand,
Expand All @@ -116,13 +116,13 @@ func TestShellCommand(t *testing.T) {
assert.Equal(t, `c:\windows\system32\cmd.exe`, strings.ToLower(command))
assert.Equal(t, []string{
"/V:ON", "/C",
"/bin/restic -v --exclude-file \"excludes\" --repo \"/path/with space\" backup .",
"\"/bin/with space/restic\" -v --exclude-file \"excludes\" --repo \"/path/with space\" backup .",
}, args)
} else {
assert.Regexp(t, regexp.MustCompile("(/usr)?/bin/(ba)?sh"), command)
assert.Equal(t, []string{
"-c",
"/bin/restic -v --exclude-file \"excludes\" --repo \"/path/with space\" backup .",
"\"/bin/with space/restic\" -v --exclude-file \"excludes\" --repo \"/path/with space\" backup .",
}, args)
}
}
Expand Down
17 changes: 16 additions & 1 deletion win/windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func RunElevated(port int) error {
constants.FlagAsChild,
constants.FlagPort,
port,
strings.Join(os.Args[1:], " "),
parseArguments(os.Args[1:]),
)

verbPtr, _ := syscall.UTF16PtrFromString(verb)
Expand All @@ -49,3 +49,18 @@ func GetConsoleWindow() windows.Handle {
ret, _, _ := getConsoleWindow.Call()
return windows.Handle(ret)
}

// parseArguments takes a slice of strings as input and returns a single string.
// It processes each argument, and if an argument contains a space, it wraps it in double quotes.
// Finally, it joins all the processed arguments into a single string separated by spaces.
func parseArguments(args []string) string {
output := make([]string, len(args))
for i, arg := range args {
if strings.Contains(arg, " ") {
output[i] = `"` + arg + `"`
continue
}
output[i] = arg
}
return strings.Join(output, " ")
}
50 changes: 50 additions & 0 deletions win/windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//go:build windows

package win

import (
"testing"
)

func TestParseArguments(t *testing.T) {
tests := []struct {
name string
args []string
expected string
}{
{
name: "NoSpaces",
args: []string{"arg1", "arg2", "arg3"},
expected: "arg1 arg2 arg3",
},
{
name: "WithSpaces",
args: []string{"arg1", "arg 2", "arg3"},
expected: `arg1 "arg 2" arg3`,
},
{
name: "AllWithSpaces",
args: []string{"arg 1", "arg 2", "arg 3"},
expected: `"arg 1" "arg 2" "arg 3"`,
},
{
name: "EmptyArgs",
args: []string{},
expected: "",
},
{
name: "MixedArgs",
args: []string{"arg1", "arg 2", "arg3", "arg 4"},
expected: `arg1 "arg 2" arg3 "arg 4"`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := parseArguments(tt.args)
if result != tt.expected {
t.Errorf("parseArguments(%v) = %v; expected %v", tt.args, result, tt.expected)
}
})
}
}
5 changes: 4 additions & 1 deletion wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,10 @@ func (r *resticWrapper) prepareCommand(command string, args *shell.Args, allowEx
return fmt.Sprintf("starting command: %s %s%s", r.ctx.binary, strings.Join(publicArguments, " "), wd)
})

rCommand := newShellCommand(r.ctx.binary, arguments, env, r.getShell(), r.dryRun, r.sigChan, r.setPID)
// creates an argument to escape the path properly
binary := shell.NewArg(r.ctx.binary, shell.ArgConfigEscape).String()

rCommand := newShellCommand(binary, arguments, env, r.getShell(), r.dryRun, r.sigChan, r.setPID)
rCommand.publicArgs = publicArguments
// stdout are stderr are coming from the default terminal (in case they're redirected)
rCommand.stdout = term.GetOutput()
Expand Down
18 changes: 18 additions & 0 deletions wrapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1728,3 +1728,21 @@ func TestCopySnapshot(t *testing.T) {
cmd := wrapper.prepareCommand("copy", args, false)
assert.Equal(t, []string{"copy", "snapshot1", "snapshot2"}, cmd.args)
}

func TestPrepareCommandShouldEscapeBinary(t *testing.T) {
if platform.IsWindows() {
t.Skip("not supported on Windows")
}
t.Parallel()

profile := config.NewProfile(&config.Config{}, "name")
ctx := &Context{
binary: "/full path to/restic",
profile: profile,
command: "backup",
}
wrapper := newResticWrapper(ctx)
args := shell.NewArgs()
cmd := wrapper.prepareCommand("backup", args, false)
assert.Equal(t, `/full\ path\ to/restic`, cmd.command)
}

0 comments on commit fb6c7a2

Please sign in to comment.