fix(github): address self-review findings from 1194bc75
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 17s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 51s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m22s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m36s
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 17s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 51s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m22s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m36s
- Handle io.ReadAll error on error body read (client.go:265) - Remove unused State field from commitStatusResponse (pr.go) - Guard via slice access in defaultCheckRedirect (client.go:117) - Move GetFileContentAtRef from pr.go to files.go (logical home)
This commit is contained in:
+9
-1
@@ -114,6 +114,11 @@ func defaultCheckRedirect(req *http.Request, via []*http.Request) error {
|
|||||||
if len(via) >= 10 {
|
if len(via) >= 10 {
|
||||||
return fmt.Errorf("stopped after 10 redirects")
|
return fmt.Errorf("stopped after 10 redirects")
|
||||||
}
|
}
|
||||||
|
// Guard: net/http guarantees len(via) >= 1 but this is undocumented;
|
||||||
|
// defend against zero-length to avoid panic on index out of range.
|
||||||
|
if len(via) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
prev := via[len(via)-1]
|
prev := via[len(via)-1]
|
||||||
// Reject protocol downgrade: HTTPS→HTTP leaks request metadata over plaintext.
|
// Reject protocol downgrade: HTTPS→HTTP leaks request metadata over plaintext.
|
||||||
if prev.URL.Scheme == "https" && req.URL.Scheme == "http" {
|
if prev.URL.Scheme == "https" && req.URL.Scheme == "http" {
|
||||||
@@ -262,7 +267,10 @@ func (c *Client) doRequest(ctx context.Context, method, reqURL string, accept st
|
|||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
errBody, _ := io.ReadAll(io.LimitReader(resp.Body, maxErrorBodyBytes))
|
errBody, readErr := io.ReadAll(io.LimitReader(resp.Body, maxErrorBodyBytes))
|
||||||
|
if readErr != nil && len(errBody) == 0 {
|
||||||
|
errBody = []byte(fmt.Sprintf("[error reading response body: %v]", readErr))
|
||||||
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
lastErr = &APIError{StatusCode: resp.StatusCode, Body: string(errBody)}
|
lastErr = &APIError{StatusCode: resp.StatusCode, Body: string(errBody)}
|
||||||
|
|||||||
@@ -17,6 +17,39 @@ func (c *Client) GetFileContent(ctx context.Context, owner, repo, path, ref stri
|
|||||||
return c.GetFileContentAtRef(ctx, owner, repo, path, ref)
|
return c.GetFileContentAtRef(ctx, owner, repo, path, ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFileContentAtRef fetches a file at a specific ref from a repo.
|
||||||
|
// If ref is empty, the query parameter is omitted (uses default branch).
|
||||||
|
//
|
||||||
|
// Note: dot-segments ("." and "..") in the path are silently removed to
|
||||||
|
// prevent path traversal. This means a path like "foo/../bar" resolves
|
||||||
|
// to "foo/bar" rather than "bar".
|
||||||
|
func (c *Client) GetFileContentAtRef(ctx context.Context, owner, repo, path, ref string) (string, error) {
|
||||||
|
reqURL := fmt.Sprintf("%s/repos/%s/%s/contents/%s",
|
||||||
|
c.baseURL, url.PathEscape(owner), url.PathEscape(repo), escapePath(path))
|
||||||
|
if ref != "" {
|
||||||
|
reqURL += "?ref=" + url.QueryEscape(ref)
|
||||||
|
}
|
||||||
|
body, err := c.doGet(ctx, reqURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("fetch file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
Encoding string `json:"encoding"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &resp); err != nil {
|
||||||
|
return "", fmt.Errorf("parse file content JSON: %w", err)
|
||||||
|
}
|
||||||
|
if resp.Encoding != "base64" {
|
||||||
|
return "", fmt.Errorf("unexpected encoding %q for file %s", resp.Encoding, path)
|
||||||
|
}
|
||||||
|
decoded, err := decodeBase64Content(resp.Content)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("decode base64 content for %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListContents lists files and directories at a given path in a repo.
|
// ListContents lists files and directories at a given path in a repo.
|
||||||
// Returns the directory listing from the GitHub contents API.
|
// Returns the directory listing from the GitHub contents API.
|
||||||
// If the path points to a single file (not a directory), the API returns
|
// If the path points to a single file (not a directory), the API returns
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ type changedFileResponse struct {
|
|||||||
|
|
||||||
// commitStatusResponse is the GitHub combined status API response.
|
// commitStatusResponse is the GitHub combined status API response.
|
||||||
type commitStatusResponse struct {
|
type commitStatusResponse struct {
|
||||||
State string `json:"state"`
|
|
||||||
Statuses []struct {
|
Statuses []struct {
|
||||||
Context string `json:"context"`
|
Context string `json:"context"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
@@ -123,39 +122,6 @@ func (c *Client) GetPullRequestFiles(ctx context.Context, owner, repo string, nu
|
|||||||
return allFiles, nil
|
return allFiles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFileContentAtRef fetches a file at a specific ref from a repo.
|
|
||||||
// If ref is empty, the query parameter is omitted (uses default branch).
|
|
||||||
//
|
|
||||||
// Note: dot-segments ("." and "..") in the path are silently removed to
|
|
||||||
// prevent path traversal. This means a path like "foo/../bar" resolves
|
|
||||||
// to "foo/bar" rather than "bar".
|
|
||||||
func (c *Client) GetFileContentAtRef(ctx context.Context, owner, repo, path, ref string) (string, error) {
|
|
||||||
reqURL := fmt.Sprintf("%s/repos/%s/%s/contents/%s",
|
|
||||||
c.baseURL, url.PathEscape(owner), url.PathEscape(repo), escapePath(path))
|
|
||||||
if ref != "" {
|
|
||||||
reqURL += "?ref=" + url.QueryEscape(ref)
|
|
||||||
}
|
|
||||||
body, err := c.doGet(ctx, reqURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("fetch file %s: %w", path, err)
|
|
||||||
}
|
|
||||||
var resp struct {
|
|
||||||
Content string `json:"content"`
|
|
||||||
Encoding string `json:"encoding"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(body, &resp); err != nil {
|
|
||||||
return "", fmt.Errorf("parse file content JSON: %w", err)
|
|
||||||
}
|
|
||||||
if resp.Encoding != "base64" {
|
|
||||||
return "", fmt.Errorf("unexpected encoding %q for file %s", resp.Encoding, path)
|
|
||||||
}
|
|
||||||
decoded, err := decodeBase64Content(resp.Content)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("decode base64 content for %s: %w", path, err)
|
|
||||||
}
|
|
||||||
return decoded, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCommitStatuses fetches both commit statuses and check runs for a SHA,
|
// GetCommitStatuses fetches both commit statuses and check runs for a SHA,
|
||||||
// merging them into a unified []vcs.CommitStatus slice.
|
// merging them into a unified []vcs.CommitStatus slice.
|
||||||
// Returns nil (not an empty slice) when there are no statuses or check runs.
|
// Returns nil (not an empty slice) when there are no statuses or check runs.
|
||||||
|
|||||||
Reference in New Issue
Block a user