diff --git a/internal/datafs/reader.go b/internal/datafs/reader.go index a7c00d34c..68d5d37f7 100644 --- a/internal/datafs/reader.go +++ b/internal/datafs/reader.go @@ -188,8 +188,6 @@ func (d *dsReader) readFileContent(ctx context.Context, u *url.URL, hdr http.Hea return &content{contentType: mimeType, b: data}, nil } -// COPIED FROM /data/datasource.go -// // resolveURL parses the relative URL rel against base, and returns the // resolved URL. Differs from url.ResolveReference in that query parameters are // added. In case of duplicates, params from rel are used. @@ -232,9 +230,18 @@ func resolveURL(base *url.URL, rel string) (*url.URL, error) { // URL.ResolveReference requires (or assumes, at least) that the base is // absolute. We want to support relative URLs too though, so we need to // correct for that. - out := base.ResolveReference(relURL) - if out.Scheme == "" && out.Path[0] == '/' { + var out *url.URL + switch { + case rel == "": + out = base + case base.IsAbs(): + out = base.ResolveReference(relURL) + case base.Scheme == "" && base.Path[0] == '/': + // absolute path, no scheme or volume + out = base.ResolveReference(relURL) out.Path = out.Path[1:] + default: + out = resolveRelativeURL(base, relURL) } if base.RawQuery != "" { @@ -248,3 +255,22 @@ func resolveURL(base *url.URL, rel string) (*url.URL, error) { return out, nil } + +// relative path, might even involve .. or . in the path +func resolveRelativeURL(base, relURL *url.URL) *url.URL { + // first find the difference between base and what base would be if it + // were absolute + emptyURL, _ := url.Parse("") + absBase := base.ResolveReference(emptyURL) + absBase.Path = strings.TrimPrefix(absBase.Path, "/") + + diff := strings.TrimSuffix(base.Path, absBase.Path) + diff = strings.TrimSuffix(diff, "/") + + out := base.ResolveReference(relURL) + + // now correct the path by adding the prefix back in + out.Path = diff + out.Path + + return out +} diff --git a/internal/datafs/reader_test.go b/internal/datafs/reader_test.go index 588592fd0..05472642c 100644 --- a/internal/datafs/reader_test.go +++ b/internal/datafs/reader_test.go @@ -61,10 +61,24 @@ func TestResolveURL(t *testing.T) { _, err = resolveURL(mustParseURL("git+ssh://git@example.com/foo//bar"), "baz//myfile") require.Error(t, err) - // relative urls must remain relative + // relative base URLs must remain relative out, err = resolveURL(mustParseURL("tmp/foo.json"), "") require.NoError(t, err) assert.Equal(t, "tmp/foo.json", out.String()) + + // relative implicit file URLs without volume or scheme are OK + out, err = resolveURL(mustParseURL("/tmp/"), "foo.json") + require.NoError(t, err) + assert.Equal(t, "tmp/foo.json", out.String()) + + // relative base URLs in parent directories are OK + out, err = resolveURL(mustParseURL("../../tmp/foo.json"), "") + require.NoError(t, err) + assert.Equal(t, "../../tmp/foo.json", out.String()) + + out, err = resolveURL(mustParseURL("../../tmp/"), "sub/foo.json") + require.NoError(t, err) + assert.Equal(t, "../../tmp/sub/foo.json", out.String()) } func TestReadFileContent(t *testing.T) { diff --git a/internal/tests/integration/datasources_file_test.go b/internal/tests/integration/datasources_file_test.go index 37e65a0fc..ea4f02592 100644 --- a/internal/tests/integration/datasources_file_test.go +++ b/internal/tests/integration/datasources_file_test.go @@ -189,3 +189,36 @@ func TestDatasources_File_Directory(t *testing.T) { withDir(tmpDir.Path()).run() assertSuccess(t, o, e, err, "core.yaml root key: cloud") } + +func TestDatsources_File_RelativePath(t *testing.T) { + // regression test for #2230 + tmpDir := fs.NewDir(t, "gomplate-inttests", + fs.WithDir("root", + fs.WithDir("foo", + fs.WithDir("bar", + fs.WithFiles(map[string]string{ + ".gomplate.yaml": ` +context: + qux: + url: "../../baz/qux.yaml" +`, + }), + ), + ), + fs.WithDir("baz", + fs.WithFiles(map[string]string{ + "qux.yaml": `values: +- value1 +- value2 +`, + }), + ), + ), + ) + t.Cleanup(tmpDir.Remove) + + o, e, err := cmd(t, "--config", ".gomplate.yaml", "-i", "{{ .qux.values }}"). + withDir(tmpDir.Join("root", "foo", "bar")).run() + + assertSuccess(t, o, e, err, "[value1 value2]") +}