From 4107944f39724c1bccfa9c2f16d7f5c3ee585713 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 7 Feb 2025 13:12:04 +0500 Subject: [PATCH 1/2] [gh api] Escape package name (URL encoding) for packages endpoint --- pkg/cmd/api/api.go | 17 +++++++++++ pkg/cmd/api/api_test.go | 66 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index a6eb69718..99b4fb659 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 = escapePathWithPackageName(opts.RequestPath) + if runF != nil { return runF(&opts) } @@ -689,3 +692,17 @@ func previewNamesToMIMETypes(names []string) string { } return strings.Join(types, ", ") } + +var pathWithPackageNameRE = regexp.MustCompile(`^\/(?:orgs|user|users)(?:\/.*)?\/packages\/(?:npm|maven|rubygems|docker|nuget|container)\/(?.*?)(?:\/(?:restore|versions)|$)`) + +func escapePathWithPackageName(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 a565312cd..7ba03932c 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) { From 9c87069199c424dbc0f6e257ee84f19902efbe62 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Tue, 25 Feb 2025 10:37:14 +0500 Subject: [PATCH 2/2] Add docs; rename function name --- pkg/cmd/api/api.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index 99b4fb659..86b26fbfc 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -265,7 +265,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command return err } - opts.RequestPath = escapePathWithPackageName(opts.RequestPath) + opts.RequestPath = escapePackageNameInPath(opts.RequestPath) if runF != nil { return runF(&opts) @@ -693,9 +693,29 @@ 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 escapePathWithPackageName(path string) string { +func escapePackageNameInPath(path string) string { matches := pathWithPackageNameRE.FindStringSubmatch(path) if len(matches) > 0 { i := pathWithPackageNameRE.SubexpIndex("package")