fix(hashline): rejected line suffixes after snapshot tags
Rejected headers such as ¶src/file.ts#1A2B:42 as malformed headers instead of treating the whole tail as a hashless path and failing later in body parsing. Added strict and apply_patch-recovery regression coverage for line-suffixed snapshot tags.
This commit is contained in:
@@ -73,10 +73,11 @@ function tryParseRecoveryHeader(line: string, cwd?: string): RawSection | null {
|
||||
}
|
||||
|
||||
// Same anti-junk rule as the strict tokenizer: a `#XXXX` token followed
|
||||
// by whitespace+content inside the path body is a malformed header (e.g.
|
||||
// stale-tag copy-paste like `src/a.ts#1A2B copied from read`), not a
|
||||
// path with an embedded hex fragment.
|
||||
if (new RegExp(`#[0-9A-Fa-f]{${HL_FILE_HASH_LENGTH}}\\s`).test(pathText)) return null;
|
||||
// by whitespace+content or a line suffix inside the path body is a
|
||||
// malformed header (e.g. stale-tag copy-paste like
|
||||
// `src/a.ts#1A2B copied from read` or `src/a.ts#1A2B:42`), not a path
|
||||
// with an embedded hex fragment.
|
||||
if (new RegExp(`#[0-9A-Fa-f]{${HL_FILE_HASH_LENGTH}}(?:\\s|:)`).test(pathText)) return null;
|
||||
|
||||
const path = normalizeHashlinePath(pathText, cwd);
|
||||
if (path.length === 0) return null;
|
||||
|
||||
@@ -334,10 +334,11 @@ function tryParseHeader(line: string): { path: string; fileHash?: string } | nul
|
||||
}
|
||||
}
|
||||
|
||||
// Reject stale-tag copy-paste such as `¶src/a.ts#1A2B copied from read`:
|
||||
// a `#XXXX` token followed by whitespace+more content in the path body
|
||||
// is a malformed header, not a path-with-embedded-hash. Surface the
|
||||
// focused diagnostic instead of silently mis-routing the edit.
|
||||
// Reject stale-tag copy-paste such as `¶src/a.ts#1A2B copied from read`
|
||||
// and line-suffixed tags such as `¶src/a.ts#1A2B:42`: a `#XXXX`
|
||||
// token followed by tag-tail junk in the path body is a malformed header,
|
||||
// not a path-with-embedded-hash. Surface the focused diagnostic instead
|
||||
// of silently mis-routing the edit.
|
||||
for (let i = FILE_PREFIX_LENGTH; i + HL_FILE_HASH_LENGTH < pathEnd; i++) {
|
||||
if (line.charCodeAt(i) !== CHAR_HASH) continue;
|
||||
let allHex = true;
|
||||
@@ -349,7 +350,9 @@ function tryParseHeader(line: string): { path: string; fileHash?: string } | nul
|
||||
}
|
||||
if (!allHex) continue;
|
||||
const after = i + HL_FILE_HASH_LENGTH + 1;
|
||||
if (after < pathEnd && isWhitespaceCode(line.charCodeAt(after))) return null;
|
||||
if (after < pathEnd && (isWhitespaceCode(line.charCodeAt(after)) || line.charCodeAt(after) === CHAR_COLON)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (pathEnd === FILE_PREFIX_LENGTH) return null;
|
||||
|
||||
@@ -28,12 +28,16 @@ describe("hashline section headers", () => {
|
||||
expect(() => Patch.parse("¶src/a.ts#1A2B copied from read\nreplace 1..1:\n+after")).toThrow(
|
||||
/Input header must be/,
|
||||
);
|
||||
expect(() => Patch.parse("¶src/a.ts#1A2B:812\nreplace 1..1:\n+after")).toThrow(/Input header must be/);
|
||||
});
|
||||
|
||||
it("rejects trailing junk after a snapshot tag even with apply_patch noise", () => {
|
||||
expect(() => Patch.parse("¶Update File: src/a.ts#1A2B copied from read\nreplace 1..1:\n+after")).toThrow(
|
||||
/Input header must be/,
|
||||
);
|
||||
expect(() => Patch.parse("¶Update File: src/a.ts#1A2B:812\nreplace 1..1:\n+after")).toThrow(
|
||||
/Input header must be/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user