From 5c6758e9906e068dd84a5cce2b8927c66418a6a9 Mon Sep 17 00:00:00 2001 From: Rodin Date: Fri, 15 May 2026 00:24:28 -0700 Subject: [PATCH] =?UTF-8?q?fix(#141):=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20tighten=20escape=20check,=20improve=20error=20messa?= =?UTF-8?q?ges,=20add=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/review-bot/validatedocmap.go | 15 +++++++++++++-- cmd/review-bot/validatedocmap_test.go | 6 ++++++ review/docmap.go | 10 +++++----- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/cmd/review-bot/validatedocmap.go b/cmd/review-bot/validatedocmap.go index 00e8cc1..69b2782 100644 --- a/cmd/review-bot/validatedocmap.go +++ b/cmd/review-bot/validatedocmap.go @@ -68,8 +68,15 @@ func runValidateDocmap(args []string) int { failed := false // --- Check 1: Coverage --- + // Note: an empty docmap (no mappings) means every changed file is + // uncovered — there are no patterns to match against. This is intentional: + // if you declare a doc-map, every changed file must be accounted for. + // On empty stdin the check is vacuously true (no files to cover). var uncovered []string for _, f := range changedFiles { + // Normalize Windows-style backslashes to forward slashes so that + // changed-file paths from git on Windows match doc-map globs. + f = strings.ReplaceAll(f, "\\", "/") if !review.FileCoveredByDocMap(cfg, f) { uncovered = append(uncovered, f) } @@ -93,7 +100,11 @@ func runValidateDocmap(args []string) int { } resolvedRoot, err := filepath.EvalSymlinks(absRoot) if err != nil { - fmt.Fprintf(errWriter, "Error: failed to resolve --repo-root %q: %v\n", *repoRootFlag, err) + if os.IsNotExist(err) { + fmt.Fprintf(errWriter, "Error: --repo-root %q does not exist\n", *repoRootFlag) + } else { + fmt.Fprintf(errWriter, "Error: failed to resolve --repo-root %q: %v\n", *repoRootFlag, err) + } return 2 } // checkStaleDocs validates each path before touching the filesystem; see @@ -151,7 +162,7 @@ func checkStaleDocs(cfg *review.DocMapConfig, repoRoot string) []string { // filepath.Rel check confirms the path is still under repoRoot. fullPath := filepath.Clean(filepath.Join(repoRoot, filepath.FromSlash(docPath))) rel, err := filepath.Rel(repoRoot, fullPath) - if err != nil || strings.HasPrefix(rel, "..") { + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { stale = append(stale, docPath) continue } diff --git a/cmd/review-bot/validatedocmap_test.go b/cmd/review-bot/validatedocmap_test.go index b2aa704..08b9f1d 100644 --- a/cmd/review-bot/validatedocmap_test.go +++ b/cmd/review-bot/validatedocmap_test.go @@ -131,6 +131,12 @@ mappings: } // stdinValidateDocmap runs runValidateDocmap with a synthetic stdin. +// +// Implementation note: we write stdinContent to a temp file and point +// os.Stdin at it. The defer f.Close() fires after stdinValidateDocmap +// returns, which is after runValidateDocmap has finished reading stdin +// synchronously — so the file is not closed while still in use. +// Tests must not call t.Parallel() while sharing the global os.Stdin. func stdinValidateDocmap(t *testing.T, stdinContent string, args []string) (code int, stdout, stderr string) { t.Helper() // Write stdin content to a temp file and redirect os.Stdin. diff --git a/review/docmap.go b/review/docmap.go index 761dabf..4381329 100644 --- a/review/docmap.go +++ b/review/docmap.go @@ -310,11 +310,11 @@ func readFileBytes(path string) ([]byte, error) { return os.ReadFile(path) } -// ValidateDocPath rejects doc paths that could cause path traversal via the -// VCS API (absolute paths, any ".." segment, backslashes). 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. Backslashes are rejected -// explicitly to prevent Windows platform edge cases. +// ValidateDocPath rejects doc paths that could cause path traversal +// (absolute paths, any ".." segment, backslashes). Defense-in-depth: callers +// must also confine the joined path to the repo root via filepath.Rel before +// any filesystem access. Backslashes are rejected explicitly to prevent +// Windows platform edge cases. func ValidateDocPath(p string) error { if strings.Contains(p, "\\") { return fmt.Errorf("backslashes not allowed in doc paths")