fix(#137): address review findings in docmap.go
- Fix package comment collision: convert to file comment (not package doc) - Add debug log for directory expansion failure before single-file fallback - Add validateDocPath: reject absolute paths and '..' segments (security #3) - Update globMatch comment to say 'filepath.Match' not 'path.Match' (gpt nit #3) - Add duplication note to truncateUTF8 explaining why it's kept separate (sonnet #2)
This commit is contained in:
+34
-5
@@ -1,5 +1,4 @@
|
|||||||
// Package review provides doc-map parsing and doc injection for path-scoped
|
// doc-map parsing and doc injection for path-scoped design document context in AI code reviews.
|
||||||
// design document context in AI code reviews.
|
|
||||||
package review
|
package review
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -106,7 +105,7 @@ func mappingMatches(patterns, files []string) bool {
|
|||||||
|
|
||||||
// globMatch matches a path against a glob pattern that may contain **.
|
// globMatch matches a path against a glob pattern that may contain **.
|
||||||
// It supports:
|
// It supports:
|
||||||
// - Standard path.Match patterns (*, ?, [range])
|
// - filepath.Match patterns (*, ?, [range])
|
||||||
// - ** as a path segment that matches zero or more segments
|
// - ** as a path segment that matches zero or more segments
|
||||||
// - Trailing /** to match a directory and all its contents
|
// - Trailing /** to match a directory and all its contents
|
||||||
//
|
//
|
||||||
@@ -246,9 +245,13 @@ type docEntry struct {
|
|||||||
// If the path is a directory, all .md files under it are returned.
|
// If the path is a directory, all .md files under it are returned.
|
||||||
// If it's a file, a single entry is returned.
|
// If it's a file, a single entry is returned.
|
||||||
func loadDocEntries(ctx context.Context, fetcher DocFetcher, owner, repo, docPath string) ([]docEntry, error) {
|
func loadDocEntries(ctx context.Context, fetcher DocFetcher, owner, repo, docPath string) ([]docEntry, error) {
|
||||||
|
if err := validateDocPath(docPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("doc path %q rejected: %w", docPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
// Try directory expansion first.
|
// Try directory expansion first.
|
||||||
files, err := fetcher.GetAllFilesInPath(ctx, owner, repo, docPath)
|
files, dirErr := fetcher.GetAllFilesInPath(ctx, owner, repo, docPath)
|
||||||
if err == nil && len(files) > 0 {
|
if dirErr == nil && len(files) > 0 {
|
||||||
// Filter for .md files only.
|
// Filter for .md files only.
|
||||||
var entries []docEntry
|
var entries []docEntry
|
||||||
for path, content := range files {
|
for path, content := range files {
|
||||||
@@ -261,6 +264,11 @@ func loadDocEntries(ctx context.Context, fetcher DocFetcher, owner, repo, docPat
|
|||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Directory expansion returned nothing; log and fall through to single-file fetch.
|
||||||
|
if dirErr != nil {
|
||||||
|
slog.Debug("doc-map: directory expansion failed, trying as single file", "path", docPath, "error", dirErr)
|
||||||
|
}
|
||||||
|
|
||||||
// Try as a single file.
|
// Try as a single file.
|
||||||
content, fileErr := fetcher.GetFileContent(ctx, owner, repo, docPath)
|
content, fileErr := fetcher.GetFileContent(ctx, owner, repo, docPath)
|
||||||
if fileErr != nil {
|
if fileErr != nil {
|
||||||
@@ -290,8 +298,29 @@ func readFileBytes(path string) ([]byte, error) {
|
|||||||
return os.ReadFile(path)
|
return os.ReadFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateDocPath rejects doc paths that could cause path traversal via the
|
||||||
|
// VCS API (absolute paths, any ".." segment). Defense-in-depth: the VCS API
|
||||||
|
// should already scope paths to the repo, but we validate locally to avoid
|
||||||
|
// any quirk in backend path handling.
|
||||||
|
func validateDocPath(p string) error {
|
||||||
|
if filepath.IsAbs(p) {
|
||||||
|
return fmt.Errorf("absolute paths not allowed")
|
||||||
|
}
|
||||||
|
for _, segment := range strings.Split(p, "/") {
|
||||||
|
if segment == ".." {
|
||||||
|
return fmt.Errorf("path traversal ('..' segment) not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// truncateUTF8 truncates s to at most maxBytes without splitting multi-byte
|
// truncateUTF8 truncates s to at most maxBytes without splitting multi-byte
|
||||||
// UTF-8 characters. Returns a valid UTF-8 string of at most maxBytes bytes.
|
// UTF-8 characters. Returns a valid UTF-8 string of at most maxBytes bytes.
|
||||||
|
//
|
||||||
|
// Note: an identical implementation exists in budget/budget.go. The two
|
||||||
|
// packages are intentionally separate (review does not import budget), so
|
||||||
|
// the duplication is accepted rather than introducing a shared internal
|
||||||
|
// package for a single small function.
|
||||||
func truncateUTF8(s string, maxBytes int) string {
|
func truncateUTF8(s string, maxBytes int) string {
|
||||||
if len(s) <= maxBytes {
|
if len(s) <= maxBytes {
|
||||||
return s
|
return s
|
||||||
|
|||||||
Reference in New Issue
Block a user