From bee1ede6544e4bbaccb6e521b5656c175a66fe7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=83=A7=E6=B4=97=E5=8F=91=E7=94=A8=E9=A3=98?= =?UTF-8?q?=E6=9F=94?= <2906266135@qq.com> Date: Mon, 7 Sep 2026 19:24:43 +0800 Subject: [PATCH 1/3] fix: limit special directory detection to template actions --- internal/filepathext/filepathext.go | 18 +++++++--- internal/filepathext/filepathext_test.go | 45 ++++++++++++++++++++++++ taskfile/node_file_test.go | 33 +++++++++++++++++ 3 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 internal/filepathext/filepathext_test.go create mode 100644 taskfile/node_file_test.go diff --git a/internal/filepathext/filepathext.go b/internal/filepathext/filepathext.go index f2a1ba15a0..947d52b927 100644 --- a/internal/filepathext/filepathext.go +++ b/internal/filepathext/filepathext.go @@ -32,12 +32,22 @@ var knownAbsDirs = []string{ } func isSpecialDir(dir string) bool { - for _, d := range knownAbsDirs { - if strings.Contains(dir, d) { - return true + for { + _, action, ok := strings.Cut(dir, "{{") + if !ok { + return false + } + action, dir, ok = strings.Cut(action, "}}") + if !ok { + return false + } + // Only inspect template actions, not literal path components. + for _, d := range knownAbsDirs { + if strings.Contains(action, d) { + return true + } } } - return false } // TryAbsToRel tries to convert an absolute path to relative based on the diff --git a/internal/filepathext/filepathext_test.go b/internal/filepathext/filepathext_test.go new file mode 100644 index 0000000000..9de492fb14 --- /dev/null +++ b/internal/filepathext/filepathext_test.go @@ -0,0 +1,45 @@ +package filepathext + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSmartJoinRelativePathContainingSpecialVariableName(t *testing.T) { + t.Parallel() + + base := t.TempDir() + for _, variable := range []string{".ROOT_DIR", ".TASKFILE_DIR", ".USER_WORKING_DIR"} { + t.Run(variable, func(t *testing.T) { + t.Parallel() + for _, relative := range []string{ + filepath.Join("project"+variable, "file.txt"), + filepath.Join(variable, "file.txt"), + filepath.Join("{{.PROJECT}}"+variable, "file.txt"), + } { + require.False(t, IsAbs(relative), relative) + require.Equal(t, filepath.Join(base, relative), SmartJoin(base, relative)) + } + }) + } +} + +func TestSmartJoinAbsoluteAndTemplatePaths(t *testing.T) { + t.Parallel() + + base := t.TempDir() + absolute := filepath.Join(t.TempDir(), "file.txt") + for _, path := range []string{ + absolute, + "{{.ROOT_DIR}}/file.txt", + "{{ .TASKFILE_DIR }}/file.txt", + "{{- .USER_WORKING_DIR -}}/file.txt", + "{{.ROOT_DIR | toSlash}}/file.txt", + "{{.PROJECT}}/{{.ROOT_DIR}}/file.txt", + } { + require.True(t, IsAbs(path), path) + require.Equal(t, path, SmartJoin(base, path)) + } +} diff --git a/taskfile/node_file_test.go b/taskfile/node_file_test.go new file mode 100644 index 0000000000..8f8552d550 --- /dev/null +++ b/taskfile/node_file_test.go @@ -0,0 +1,33 @@ +package taskfile + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFileNodeResolveLiteralSpecialDir(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + entrypoint := filepath.Join(dir, "Taskfile.yml") + require.NoError(t, os.WriteFile(entrypoint, []byte("version: '3'\n"), 0o600)) + node, err := NewFileNode(entrypoint, "") + require.NoError(t, err) + + for _, name := range []string{"project.ROOT_DIR", "project.TASKFILE_DIR", "project.USER_WORKING_DIR"} { + t.Run(name, func(t *testing.T) { + t.Parallel() + + resolvedDir, err := node.ResolveDir(name) + require.NoError(t, err) + require.Equal(t, filepath.Join(dir, name), resolvedDir) + + resolvedEntrypoint, err := node.ResolveEntrypoint(filepath.Join(name, "Taskfile.yml")) + require.NoError(t, err) + require.Equal(t, filepath.Join(dir, name, "Taskfile.yml"), resolvedEntrypoint) + }) + } +} From 7f7b87f31d8c51aa64db2e312f2caac77beccf76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=83=A7=E6=B4=97=E5=8F=91=E7=94=A8=E9=A3=98?= =?UTF-8?q?=E6=9F=94?= <2906266135@qq.com> Date: Tue, 8 Sep 2026 17:42:22 +0800 Subject: [PATCH 2/3] refactor: precompile special directory template matching --- internal/filepathext/filepathext.go | 26 ++++-------------------- internal/filepathext/filepathext_test.go | 19 +++++++++++++++++ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/internal/filepathext/filepathext.go b/internal/filepathext/filepathext.go index 947d52b927..23356fc193 100644 --- a/internal/filepathext/filepathext.go +++ b/internal/filepathext/filepathext.go @@ -3,7 +3,7 @@ package filepathext import ( "os" "path/filepath" - "strings" + "regexp" ) // SmartJoin joins two paths, but only if the second is not already an @@ -25,29 +25,11 @@ func IsAbs(path string) bool { return filepath.IsAbs(path) } -var knownAbsDirs = []string{ - ".ROOT_DIR", - ".TASKFILE_DIR", - ".USER_WORKING_DIR", -} +// Match special directory variables only within a template action. +var specialDirRE = regexp.MustCompile(`\{\{[^{}]*\.(?:ROOT_DIR|TASKFILE_DIR|USER_WORKING_DIR)[^{}]*\}\}`) func isSpecialDir(dir string) bool { - for { - _, action, ok := strings.Cut(dir, "{{") - if !ok { - return false - } - action, dir, ok = strings.Cut(action, "}}") - if !ok { - return false - } - // Only inspect template actions, not literal path components. - for _, d := range knownAbsDirs { - if strings.Contains(action, d) { - return true - } - } - } + return specialDirRE.MatchString(dir) } // TryAbsToRel tries to convert an absolute path to relative based on the diff --git a/internal/filepathext/filepathext_test.go b/internal/filepathext/filepathext_test.go index 9de492fb14..3c3a9719dc 100644 --- a/internal/filepathext/filepathext_test.go +++ b/internal/filepathext/filepathext_test.go @@ -18,6 +18,8 @@ func TestSmartJoinRelativePathContainingSpecialVariableName(t *testing.T) { filepath.Join("project"+variable, "file.txt"), filepath.Join(variable, "file.txt"), filepath.Join("{{.PROJECT}}"+variable, "file.txt"), + filepath.Join("{{.PROJECT}}"+variable+"{{.SUFFIX}}", "file.txt"), + filepath.Join("{{ "+variable, "file.txt"), } { require.False(t, IsAbs(relative), relative) require.Equal(t, filepath.Join(base, relative), SmartJoin(base, relative)) @@ -38,8 +40,25 @@ func TestSmartJoinAbsoluteAndTemplatePaths(t *testing.T) { "{{- .USER_WORKING_DIR -}}/file.txt", "{{.ROOT_DIR | toSlash}}/file.txt", "{{.PROJECT}}/{{.ROOT_DIR}}/file.txt", + "{{\n.TASKFILE_DIR\n}}/file.txt", + `{{joinPath .ROOT_DIR "src"}}/file.txt`, } { require.True(t, IsAbs(path), path) require.Equal(t, path, SmartJoin(base, path)) } } + +func TestSmartJoinUnrelatedTemplatePaths(t *testing.T) { + t.Parallel() + + base := t.TempDir() + for _, path := range []string{ + "{{.PROJECT}}/file.txt", + "{{XROOT_DIR}}/file.txt", + "{{XTASKFILE_DIR}}/file.txt", + "{{XUSER_WORKING_DIR}}/file.txt", + } { + require.False(t, IsAbs(path), path) + require.Equal(t, filepath.Join(base, path), SmartJoin(base, path)) + } +} From 0623c1ca474b28f0794aab91c1dee58ac5e72f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=83=A7=E6=B4=97=E5=8F=91=E7=94=A8=E9=A3=98?= =?UTF-8?q?=E6=9F=94?= <2906266135@qq.com> Date: Tue, 8 Sep 2026 17:52:50 +0800 Subject: [PATCH 3/3] fix: reject special directory variable name suffixes --- internal/filepathext/filepathext.go | 5 +++-- internal/filepathext/filepathext_test.go | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/internal/filepathext/filepathext.go b/internal/filepathext/filepathext.go index 23356fc193..615969bf22 100644 --- a/internal/filepathext/filepathext.go +++ b/internal/filepathext/filepathext.go @@ -25,8 +25,9 @@ func IsAbs(path string) bool { return filepath.IsAbs(path) } -// Match special directory variables only within a template action. -var specialDirRE = regexp.MustCompile(`\{\{[^{}]*\.(?:ROOT_DIR|TASKFILE_DIR|USER_WORKING_DIR)[^{}]*\}\}`) +// Heuristically match special directory names within template actions, without +// matching prefixes of longer identifiers. This does not parse template syntax. +var specialDirRE = regexp.MustCompile(`\{\{[^{}]*\.(?:ROOT_DIR|TASKFILE_DIR|USER_WORKING_DIR)(?:[^\p{L}\p{Nd}_{}][^{}]*)?\}\}`) func isSpecialDir(dir string) bool { return specialDirRE.MatchString(dir) diff --git a/internal/filepathext/filepathext_test.go b/internal/filepathext/filepathext_test.go index 3c3a9719dc..cf4f014a80 100644 --- a/internal/filepathext/filepathext_test.go +++ b/internal/filepathext/filepathext_test.go @@ -39,6 +39,8 @@ func TestSmartJoinAbsoluteAndTemplatePaths(t *testing.T) { "{{ .TASKFILE_DIR }}/file.txt", "{{- .USER_WORKING_DIR -}}/file.txt", "{{.ROOT_DIR | toSlash}}/file.txt", + "{{.ROOT_DIR|toSlash}}/file.txt", + "{{(.ROOT_DIR)}}/file.txt", "{{.PROJECT}}/{{.ROOT_DIR}}/file.txt", "{{\n.TASKFILE_DIR\n}}/file.txt", `{{joinPath .ROOT_DIR "src"}}/file.txt`, @@ -62,3 +64,20 @@ func TestSmartJoinUnrelatedTemplatePaths(t *testing.T) { require.Equal(t, filepath.Join(base, path), SmartJoin(base, path)) } } + +func TestSmartJoinTemplateVariableNameSuffixes(t *testing.T) { + t.Parallel() + + base := t.TempDir() + for _, variable := range []string{".ROOT_DIR", ".TASKFILE_DIR", ".USER_WORKING_DIR"} { + for _, suffix := range []string{"_SUFFIX", "_EXTRA", "2", "suffix", "变量"} { + t.Run(variable+suffix, func(t *testing.T) { + t.Parallel() + + path := "{{" + variable + suffix + "}}/file.txt" + require.False(t, IsAbs(path), path) + require.Equal(t, filepath.Join(base, path), SmartJoin(base, path)) + }) + } + } +}