Skip to content

Commit 1f45505

Browse files
authored
feat: variables in template file names (#105)
Template file names can contain variables the same way templates can. See `./examples/variable-file-names` for an example.
1 parent b580c14 commit 1f45505

File tree

6 files changed

+91
-3
lines changed

6 files changed

+91
-3
lines changed
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: v1
2+
name: variable-filename
3+
version: v0.0.0
4+
description: Minimal recipe which just creates a file named from a variable
5+
vars:
6+
- name: README_FILE_NAME
7+
description: |
8+
The base name for the rendered file
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The name of this file is {{ .Variables.README_FILE_NAME }}.md

pkg/engine/engine.go

+19-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,25 @@ func (e Engine) Render(templates map[string][]byte, values map[string]interface{
4747

4848
// TODO: Could we detect unused variables, and give warning about those?
4949

50-
rendered[name] = []byte(output)
50+
// File names can be templates to, render them here as well
51+
52+
buf.Reset()
53+
54+
template, err := t.New("__template_file_filename").Parse(name)
55+
if err != nil {
56+
return nil, fmt.Errorf("failed to parse file name template: %w", err)
57+
}
58+
59+
if err := template.Execute(&buf, values); err != nil {
60+
return nil, fmt.Errorf("failed to execute file name template: %w", err)
61+
}
62+
63+
filename := buf.String()
64+
if strings.Contains(filename, "<no value>") {
65+
return nil, fmt.Errorf("variable for file name %q was undefined", name)
66+
}
67+
68+
rendered[filename] = []byte(output)
5169
}
5270

5371
return rendered, nil

pkg/engine/engine_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ func TestRender(t *testing.T) {
1515
{
1616
"values_and_functions",
1717
map[string][]byte{
18-
"templates/test1": []byte("{{.var1 | title }} {{.var2 | title}}"),
18+
"templates/test1": []byte("{{.var1 | title }} {{.var2 | title}}"),
19+
"templates/{{.var1}}": []byte("{{.var1}}"),
1920
},
2021
map[string]interface{}{
2122
"var1": "first",
2223
"var2": "second",
2324
},
2425
map[string][]byte{
2526
"templates/test1": []byte("First Second"),
27+
"templates/first": []byte("first"),
2628
},
2729
},
2830
{

pkg/recipe/saver.go

+25-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"strings"
78

89
"gopkg.in/yaml.v3"
910
)
@@ -13,6 +14,24 @@ const (
1314
yamlIndent int = 2
1415
)
1516

17+
func ValidatePath(basePath, filePath string) error {
18+
absBasePath, err := filepath.Abs(basePath)
19+
if err != nil {
20+
return fmt.Errorf("invalid base path %q: %v", basePath, err)
21+
}
22+
23+
absFilePath, err := filepath.Abs(filePath)
24+
if err != nil {
25+
return fmt.Errorf("invalid file path %q: %v", basePath, err)
26+
}
27+
28+
if !strings.HasPrefix(absFilePath, absBasePath) {
29+
return fmt.Errorf("file path escapes destination: %q outside %q", absFilePath, absBasePath)
30+
}
31+
32+
return nil
33+
}
34+
1635
// Save saves recipe to given destination
1736
func (re *Recipe) Save(dest string) error {
1837
err := os.MkdirAll(dest, defaultFileMode)
@@ -197,8 +216,13 @@ func saveFileMap(files map[string]File, dest string) error {
197216
for path, file := range files {
198217
destPath := filepath.Join(dest, path)
199218

219+
err := ValidatePath(dest, destPath)
220+
if err != nil {
221+
return err
222+
}
223+
200224
// Create file's parent directories (if not already exist)
201-
err := os.MkdirAll(filepath.Dir(destPath), 0700)
225+
err = os.MkdirAll(filepath.Dir(destPath), 0700)
202226
if err != nil {
203227
return err
204228
}

pkg/recipe/saver_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package recipe
33
import (
44
"os"
55
"path/filepath"
6+
"strings"
67
"testing"
78

89
"github.com/futurice/jalapeno/pkg/engine"
@@ -117,3 +118,37 @@ func TestSaveSauce(t *testing.T) {
117118
}
118119
}
119120
}
121+
122+
func TestSaveSauceDoesNotWriteOutsideDest(t *testing.T) {
123+
dir, err := os.MkdirTemp("", "jalapeno-test-saver")
124+
if err != nil {
125+
t.Fatalf("cannot create temp dir: %s", err)
126+
}
127+
defer os.RemoveAll(dir)
128+
129+
re := NewRecipe()
130+
re.Name = "Test"
131+
re.Version = "v0.0.1"
132+
re.Templates = map[string]File{
133+
"../foo.md": NewFile([]byte("foo")),
134+
}
135+
136+
err = re.Validate()
137+
if err != nil {
138+
t.Fatalf("test recipe was not valid: %s", err)
139+
}
140+
141+
sauce, err := re.Execute(engine.New(), nil, uuid.Must(uuid.NewV4()))
142+
if err != nil {
143+
t.Fatalf("recipe execution failed: %s", err)
144+
}
145+
146+
err = sauce.Save(dir)
147+
if err == nil {
148+
t.Fatalf("should not have saved sauce")
149+
}
150+
151+
if !strings.Contains(err.Error(), "file path escapes destination") {
152+
t.Fatalf("error received was not expected: %s", err)
153+
}
154+
}

0 commit comments

Comments
 (0)