diff --git a/api/internal/localizer/util.go b/api/internal/localizer/util.go index 500da8ccc..cf076e129 100644 --- a/api/internal/localizer/util.go +++ b/api/internal/localizer/util.go @@ -5,6 +5,7 @@ package localizer import ( "log" + "net/url" "path/filepath" "strings" @@ -120,8 +121,25 @@ func cleanFilePath(fSys filesys.FileSystem, root filesys.ConfirmedDir, file stri return locPath } -// locFilePath returns the relative localized path of validated file url fileURL -// TODO(annasong): implement -func locFilePath(_ string) string { - return filepath.Join(LocalizeDir, "") +// locFilePath converts a URL to its localized form, e.g. +// https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/api/krusty/testdata/localize/simple/service.yaml -> +// localized-files/raw.githubusercontent.com/kubernetes-sigs/kustomize/master/api/krusty/testdata/localize/simple/service.yaml. +// +// fileURL must be a validated file URL. +func locFilePath(fileURL string) string { + // File urls must have http or https scheme, so it is safe to use url.Parse. + u, err := url.Parse(fileURL) + if err != nil { + log.Fatalf("cannot parse validated file url %q: %s", fileURL, err.Error()) + } + + // Percent-encodings should be preserved in case sub-delims have special meaning. + // Extraneous '..' parent directory dot-segments should be removed. + path := filepath.Join(string(filepath.Separator), filepath.FromSlash(u.EscapedPath())) + + // The host should not include userinfo or port. + // Raw github urls are the only type of file urls kustomize officially accepts. + // In this case, the path already consists of org, repo, version, and path in repo, in order, + // so we can use it as is. + return filepath.Join(LocalizeDir, u.Hostname(), path) } diff --git a/api/internal/localizer/util_test.go b/api/internal/localizer/util_test.go index ca8096e73..956c46a46 100644 --- a/api/internal/localizer/util_test.go +++ b/api/internal/localizer/util_test.go @@ -4,6 +4,8 @@ package localizer //nolint:testpackage import ( + "path/filepath" + "strings" "testing" "github.com/stretchr/testify/require" @@ -16,3 +18,49 @@ func TestUrlBase(t *testing.T) { func TestUrlBaseTrailingSlash(t *testing.T) { require.Equal(t, "repo", urlBase("github.com/org/repo//")) } + +// simpleJoin is filepath.Join() without the side effects of filepath.Clean() +func simpleJoin(t *testing.T, elems ...string) string { + t.Helper() + + return strings.Join(elems, string(filepath.Separator)) +} + +func TestLocFilePath(t *testing.T) { + for name, tUnit := range map[string]struct { + url, path string + }{ + "official": { + url: "https://raw.githubusercontent.com/org/repo/ref/path/to/file.yaml", + path: simpleJoin(t, "raw.githubusercontent.com", "org", "repo", "ref", "path", "to", "file.yaml"), + }, + "empty_path": { + url: "https://host", + path: "host", + }, + "empty_path_segment": { + url: "https://host//", + path: "host", + }, + "extraneous_components": { + url: "http://userinfo@host:1234/path/file?query", + path: simpleJoin(t, "host", "path", "file"), + }, + "percent-encoding": { + url: "https://host/file%2Eyaml", + path: simpleJoin(t, "host", "file%2Eyaml"), + }, + "dot-segments": { + url: "https://host/path/blah/../to/foo/bar/../../file/./", + path: simpleJoin(t, "host", "path", "to", "file"), + }, + "extraneous_dot-segments": { + url: "https://host/foo/bar/baz/../../../../file", + path: simpleJoin(t, "host", "file"), + }, + } { + t.Run(name, func(t *testing.T) { + require.Equal(t, simpleJoin(t, LocalizeDir, tUnit.path), locFilePath(tUnit.url)) + }) + } +}