diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index 51febdfc9..fc6aabc47 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "path/filepath" "regexp" @@ -264,6 +265,8 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command return err } + opts.RequestPath = escapePackageNameInPath(opts.RequestPath) + if runF != nil { return runF(&opts) } @@ -691,3 +694,37 @@ func previewNamesToMIMETypes(names []string) string { } return strings.Join(types, ", ") } + +// The package name part in the `packages` endpoints may contain slashes and +// other characters that need to be URL encoded. +// +// The `escapePackageNameInPath` function extracts and normalizes package names +// in the path. The regex `pathWithPackageNameRE` is being used to extract the +// package name with a capture group named `package`. +// +// See https://docs.github.com/en/rest/packages/packages APIs for more details. +// +// Here's an example: +// +// The package name `orders/cache` needs to be URL encoded because it contains +// a slash `/`. The `escapePackageNameInPath` function will extract the +// `orders/cache` part, perform the URL encoding, and return the normalized API +// endpoint with `%2F` replacing the slash `/` in the package name part only. +// +// - Package name: `orders/cache` +// - API endpoint: `/users/USER/packages/container/orders/cache` +// - Normalized: `/users/USER/packages/container/orders%2Fcache` + +var pathWithPackageNameRE = regexp.MustCompile(`^\/(?:orgs|user|users)(?:\/.*)?\/packages\/(?:npm|maven|rubygems|docker|nuget|container)\/(?.*?)(?:\/(?:restore|versions)|$)`) + +func escapePackageNameInPath(path string) string { + matches := pathWithPackageNameRE.FindStringSubmatch(path) + if len(matches) > 0 { + i := pathWithPackageNameRE.SubexpIndex("package") + packageName := matches[i] + if packageName != "" { + return strings.Replace(path, packageName, url.QueryEscape(packageName), 1) + } + } + return path +} diff --git a/pkg/cmd/api/api_test.go b/pkg/cmd/api/api_test.go index 321f7b7c0..ee58d55d0 100644 --- a/pkg/cmd/api/api_test.go +++ b/pkg/cmd/api/api_test.go @@ -367,6 +367,72 @@ func Test_NewCmdApi(t *testing.T) { }, wantsErr: false, }, + { + name: "request path with container package name containing slashes", + cli: "/user/packages/container/github.com/username/package_name --verbose", + wants: ApiOptions{ + Hostname: "", + RequestMethod: "GET", + RequestMethodPassed: false, + RequestPath: "/user/packages/container/github.com%2Fusername%2Fpackage_name", + RequestInputFile: "", + RawFields: []string(nil), + MagicFields: []string(nil), + RequestHeaders: []string(nil), + ShowResponseHeaders: false, + Paginate: false, + Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", + Verbose: true, + }, + wantsErr: false, + }, + { + name: "request path with container package name containing slashes and restore", + cli: "/user/packages/container/github.com/username/package_name/restore --verbose", + wants: ApiOptions{ + Hostname: "", + RequestMethod: "GET", + RequestMethodPassed: false, + RequestPath: "/user/packages/container/github.com%2Fusername%2Fpackage_name/restore", + RequestInputFile: "", + RawFields: []string(nil), + MagicFields: []string(nil), + RequestHeaders: []string(nil), + ShowResponseHeaders: false, + Paginate: false, + Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", + Verbose: true, + }, + wantsErr: false, + }, + { + name: "request path with container package name containing slashes and versions", + cli: "/user/packages/container/github.com/username/package_name/versions --verbose", + wants: ApiOptions{ + Hostname: "", + RequestMethod: "GET", + RequestMethodPassed: false, + RequestPath: "/user/packages/container/github.com%2Fusername%2Fpackage_name/versions", + RequestInputFile: "", + RawFields: []string(nil), + MagicFields: []string(nil), + RequestHeaders: []string(nil), + ShowResponseHeaders: false, + Paginate: false, + Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", + Verbose: true, + }, + wantsErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {