Merge pull request #10384 from iamazeem/9798-gh-api-encode-package-name

[gh api] Escape package name (URL encoding) for packages endpoint
This commit is contained in:
Tyler McGoffin 2025-02-25 11:12:48 -08:00 committed by GitHub
commit ed2c322a73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 103 additions and 0 deletions

View file

@ -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)\/(?<package>.*?)(?:\/(?: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
}

View file

@ -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) {