From 7619dd5928b72b0cb3470079febb77112870f9b7 Mon Sep 17 00:00:00 2001 From: sunlei Date: Thu, 25 Jun 2026 00:22:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BC=BA=E5=8C=96NapCat=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E5=8F=AA=E8=AF=BB=E4=B8=8E=E8=B7=AF=E5=BE=84=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/selfTest.ts | 59 ++++++++++++++++++++++++++++++++-- src/tools/napcatAutomation.ts | 60 ++++++++++++++++++++++++++++++++--- 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/src/selfTest.ts b/src/selfTest.ts index c0a1a97..afdd95d 100644 --- a/src/selfTest.ts +++ b/src/selfTest.ts @@ -129,8 +129,8 @@ export async function runSelfTest(): Promise { const dryRunAuditCommandText = dryRunAuditCommands.join("\n"); for (const expected of [ "git status --short --branch", - "git fetch upstream --tags --prune", - "git fetch origin --prune", + "git fetch --dry-run upstream --tags --prune", + "git fetch --dry-run origin --prune", "git diff --name-only", "git merge-tree", ]) { @@ -140,6 +140,17 @@ export async function runSelfTest(): Promise { ); } } + for (const command of dryRunAudit.commands) { + if ( + command.readOnly && + /^git\s+fetch(?:\s|$)/.test(command.command) && + !/\s--dry-run(?:\s|$)/.test(command.command) + ) { + throw new Error( + "NapCat upstream audit read-only fetch must use --dry-run", + ); + } + } for (const forbidden of [ { label: "git merge", regex: /(^|\n)\s*git\s+merge(?:\s|$)/ }, { label: "git push", regex: /(^|\n)\s*git\s+push(?:\s|$)/ }, @@ -153,6 +164,50 @@ export async function runSelfTest(): Promise { ); } } + for (const invalidPathCase of [ + { + forkPatchFiles: [], + name: "Windows absolute path", + upstreamChangedFiles: ["D:/repo/packages/napcat-core/login/runtime.ts"], + }, + { + forkPatchFiles: [], + name: "Unix absolute path", + upstreamChangedFiles: ["/repo/packages/napcat-core/login/runtime.ts"], + }, + { + forkPatchFiles: [], + name: "UNC path", + upstreamChangedFiles: [ + "\\\\server\\share\\packages\\napcat-core\\login\\runtime.ts", + ], + }, + { + forkPatchFiles: [], + name: "parent segment path", + upstreamChangedFiles: ["packages/../napcat-core/login/runtime.ts"], + }, + { + forkPatchFiles: ["packages/../napcat-core/login/runtime.ts"], + name: "fork patch parent segment path", + upstreamChangedFiles: [], + }, + ]) { + const invalidPathAudit = classifyNapcatUpstreamAudit({ + dryMergeConflict: false, + forkPatchFiles: invalidPathCase.forkPatchFiles, + upstreamChangedFiles: invalidPathCase.upstreamChangedFiles, + }); + if ( + invalidPathAudit.classification !== "blocked" || + invalidPathAudit.recommendedAction !== "block" || + !invalidPathAudit.reasonCodes.includes("METADATA_UNAVAILABLE") + ) { + throw new Error( + `NapCat invalid audit path self-check failed: ${invalidPathCase.name}`, + ); + } + } for (const promptName of napcatAutomationPromptNames) { const promptText = loadNapcatAutomationPrompt(promptName); if (!promptText.includes("Do not edit files")) { diff --git a/src/tools/napcatAutomation.ts b/src/tools/napcatAutomation.ts index f4cc538..2cf0be5 100644 --- a/src/tools/napcatAutomation.ts +++ b/src/tools/napcatAutomation.ts @@ -75,6 +75,46 @@ const hotZoneMatchers: Array<{ pattern: string; regex: RegExp }> = [ { pattern: 'vite*.ts', regex: /(?:^|\/)vite[^/]*\.ts$/i }, ]; +/** + * Checks whether Git audit metadata contains a repo-relative path that the classifier can trust. + * @param value - Raw path string from fork/upstream Git diff collectors; empty entries are allowed because collectors may emit blank lines. + * @returns True when a non-empty path is absolute, UNC-based, or contains a parent-directory segment. + */ +function hasInvalidAuditPath(value: string): boolean { + const trimmed = value.trim(); + if (!trimmed) return false; + if (/^[a-zA-Z]:[\\/]/.test(trimmed)) return true; + if (trimmed.startsWith('\\\\') || trimmed.startsWith('//')) return true; + if (trimmed.startsWith('/')) return true; + + const normalized = trimmed + .replace(/\\/g, '/') + .replace(/^\.\/+/, '') + .replace(/\/+/g, '/'); + return normalized.split('/').includes('..'); +} + +/** + * Checks both file lists for invalid non repo-relative metadata. + * @param input - Collector result carrying fork patch and upstream changed file paths; both arrays must contain repo-relative Git paths when non-empty. + * @returns True when any path entry would make deterministic hot-zone and overlap classification untrustworthy. + */ +function hasInvalidAuditMetadata( + input: NapcatAuditClassificationInput, +): boolean { + for (const file of input.forkPatchFiles) { + if (hasInvalidAuditPath(file)) { + return true; + } + } + for (const file of input.upstreamChangedFiles) { + if (hasInvalidAuditPath(file)) { + return true; + } + } + return false; +} + /** * Normalizes Git path output into a stable comparison key for audit classifiers. * @param value - File path from Git diff/status collectors; may contain Windows separators, leading `./`, or surrounding whitespace, but must represent a repo-relative path. @@ -195,6 +235,16 @@ function buildClassificationSummary( export function classifyNapcatUpstreamAudit( input: NapcatAuditClassificationInput, ): NapcatAuditClassificationResult { + if (hasInvalidAuditMetadata(input)) { + return { + classification: 'blocked', + reasonCodes: ['METADATA_UNAVAILABLE'], + recommendedAction: 'block', + summary: + 'Invalid audit metadata: fork and upstream file paths must be repo-relative Git paths without absolute roots, UNC prefixes, or parent-directory segments.', + }; + } + if (input.dryMergeConflict) { return { classification: 'blocked', @@ -254,13 +304,15 @@ export function buildNapcatUpstreamAudit( readOnly: true, }, { - command: 'git fetch upstream --tags --prune', - description: 'Refresh upstream release refs before file-level audit collection.', + command: 'git fetch --dry-run upstream --tags --prune', + description: + 'Probe upstream release ref availability without updating local refs.', readOnly: true, }, { - command: 'git fetch origin --prune', - description: 'Refresh fork refs before comparing KT patch and upstream ranges.', + command: 'git fetch --dry-run origin --prune', + description: + 'Probe fork ref availability without updating remote-tracking refs.', readOnly: true, }, {