From b594ff78b22452b1260286f86fc5a40dbf3d38d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Tue, 16 Apr 2024 07:38:23 +0200 Subject: [PATCH] Improve shell quoting --- Makefile | 2 ++ acid.adoc | 3 +++ acid.go | 27 ++++++++++++++++++++++++--- acid.yaml.example | 6 +++--- acid_test.go | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 acid_test.go diff --git a/Makefile b/Makefile index 1fd3dc3..455c22d 100644 --- a/Makefile +++ b/Makefile @@ -9,5 +9,7 @@ acid: acid.go go build -ldflags "-X 'main.projectVersion=$(version)'" -o $@ acid.1: acid.adoc asciidoctor -b manpage -a release-version=$(version) -o $@ acid.adoc +test: all + go test clean: rm -f $(outputs) diff --git a/acid.adoc b/acid.adoc index 4c56562..6775d52 100644 --- a/acid.adoc +++ b/acid.adoc @@ -71,6 +71,9 @@ which has the following fields: *CloneURL*:: Gitea link for cloning the repository over HTTP. +The special *quote* template function quotes fields for safe usage +in *sh*(1) command arguments. + Runners ------- Runners receive the following additional environment variables: diff --git a/acid.go b/acid.go index b029c2a..2c59c38 100644 --- a/acid.go +++ b/acid.go @@ -93,10 +93,30 @@ func parseConfig(path string) error { } var err error - gNotifyScript, err = ttemplate.New("notify").Parse(gConfig.Notify) + gNotifyScript, err = + ttemplate.New("notify").Funcs(shellFuncs).Parse(gConfig.Notify) return err } +var shellFuncs = ttemplate.FuncMap{ + "quote": func(word string) string { + // History expansion is annoying, don't let it cut us. + if strings.IndexRune(word, '!') >= 0 { + return "'" + strings.ReplaceAll(word, "'", `'"'"'`) + "'" + } + + const special = "$`\"\\" + quoted := []rune{'"'} + for _, r := range word { + if strings.IndexRune(special, r) >= 0 { + quoted = append(quoted, '\\') + } + quoted = append(quoted, r) + } + return string(append(quoted, '"')) + }, +} + // --- Utilities --------------------------------------------------------------- func giteaSign(b []byte) string { @@ -910,8 +930,9 @@ func executorRunTask(ctx context.Context, task Task) error { // - we might have to clone submodules as well. // Otherwise, we could download a source archive from Gitea, // and use SFTP to upload it to the runner. - tmplScript, err := ttemplate.New("script").Parse(rt.Runner.Setup + "\n" + - rt.ProjectRunner.Setup + "\n" + rt.ProjectRunner.Build) + tmplScript, err := ttemplate.New("script").Funcs(shellFuncs). + Parse(rt.Runner.Setup + "\n" + + rt.ProjectRunner.Setup + "\n" + rt.ProjectRunner.Build) if err != nil { return fmt.Errorf("script: %w", err) } diff --git a/acid.yaml.example b/acid.yaml.example index a80cc4c..b34abf0 100644 --- a/acid.yaml.example +++ b/acid.yaml.example @@ -44,9 +44,9 @@ runners: setup: | set -ex sudo pacman -Syu --noconfirm git - git clone --recursive '{{.CloneURL}}' '{{.Repo}}' - cd '{{.Repo}}' - git -c advice.detachedHead=false checkout '{{.Hash}}' + git clone --recursive {{quote .CloneURL}} {{quote .Repo}} + cd {{quote .Repo}} + git -c advice.detachedHead=false checkout {{quote .Hash}} # Configuration for individual Gitea repositories. projects: diff --git a/acid_test.go b/acid_test.go new file mode 100644 index 0000000..f23d892 --- /dev/null +++ b/acid_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "bytes" + "testing" + ttemplate "text/template" +) + +func TestTemplateQuote(t *testing.T) { + // Ideally, we should back-parse it using sh syntax. + // This is an unnecessarily fragile test. + for _, test := range []struct { + input, output string + }{ + {`!!`, `'!!'`}, + {``, `""`}, + {`${var}`, `"\${var}"`}, + {"`cat`", "\"\\`cat\\`\""}, + {`"魚\"`, `"\"魚\\\""`}, + } { + var b bytes.Buffer + err := ttemplate.Must(ttemplate.New("test"). + Funcs(shellFuncs).Parse("{{quote .}}")).Execute(&b, test.input) + if err != nil { + t.Errorf("template execution error: %s\n", err) + } + if b.String() != test.output { + t.Errorf("%q should be quoted os %q, not %q\n", + test.input, test.output, b.String()) + } + } +}