package github import ( "context" "encoding/base64" "encoding/json" "fmt" "net/url" "strings" ) // 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 } // escapePath encodes each segment of a slash-separated path, stripping // dot-segments to prevent path traversal. func escapePath(p string) string { parts := strings.Split(p, "/") var clean []string for _, part := range parts { if part == "." || part == ".." || part == "" { continue } clean = append(clean, url.PathEscape(part)) } return strings.Join(clean, "/") } // decodeBase64Content decodes base64-encoded content from the GitHub contents API. // GitHub returns base64 content with line breaks for formatting; we strip \r and \n before decoding. func decodeBase64Content(encoded string) (string, error) { cleaned := strings.NewReplacer("\n", "", "\r", "").Replace(encoded) decoded, err := base64.StdEncoding.DecodeString(cleaned) if err != nil { return "", err } return string(decoded), nil }