fix(#2701): reject NUL-corrupted plan/state artifacts at the validator entry points (#2829)

* test(#2701): failing-first regression for NUL-corrupted plan/state validators

* fix(#2701): reject NUL-corrupted plan/state artifacts at the validator entry points

* fix(#2701): seed STATE.md in test (writeState); add NUL-path guards to validate/verify for parity (review)

* docs(changeset): #2701 validators reject NUL-corrupted artifacts

* docs(changeset): backfill #2701 PR number to 2829
This commit is contained in:
Tom Boucher
2026-07-29 12:29:45 -04:00
committed by GitHub
parent 69dbf28ca7
commit 6229f0e55c
6 changed files with 277 additions and 1 deletions

View File

@@ -11,7 +11,7 @@ import path from 'node:path';
import os from 'node:os';
import { phaseVariants, buildRoadmapPhaseVariants, buildNotStartedPhaseVariants } from './validate.cjs';
import { realClock } from './clock.cjs';
import { phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem } from './validate.cjs';
import { phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem, textEncodingError } from './validate.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
import planningWorkspace = require('./planning-workspace.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
@@ -811,6 +811,7 @@ function cmdVerifyPlanStructure(cwd: string, filePath: string, raw: boolean): vo
if (!filePath) {
error('file path required');
}
if (filePath.includes('\0')) { error('file path contains null bytes'); }
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
const content = safeReadFile(fullPath);
if (!content) {
@@ -818,6 +819,15 @@ function cmdVerifyPlanStructure(cwd: string, filePath: string, raw: boolean): vo
return;
}
// #2701: fail loud on NUL/binary corruption before structure checks. A
// structurally intact-but-NUL-corrupted plan otherwise passes as valid and is
// silently skipped by recursive/binary-skipping searchers downstream.
const encErr = textEncodingError(content, filePath);
if (encErr) {
output({ valid: false, errors: [encErr] }, raw);
return;
}
const fm = extractFrontmatter(content, fullPath);
const errors: string[] = [];
const warnings: string[] = [];