Skip to content

Commit 7142faf

Browse files
committed
Address CodeRabbit review feedback
- cad-encoding.ts: parse the DXF HEADER as (group code, value) pairs instead of regex-matching the whole 64 KiB probe. `readDxfTaggedValue` accepted the variable name anywhere in the prefix and did not require the group-9 record that introduces it, so an MTEXT entity whose own content read `1/$DWGCODEPAGE/3/ANSI_936` could supply a codepage the header never declared, and the loader would then recode every string field with it. The walk stays inside SECTION/HEADER, stops at its ENDSEC, and only treats a group-9 record as a variable name. This also drops the regex built from interpolated arguments that ast-grep flagged. - cad-encoding.ts: strip a leading UTF-8 BOM before the walk. Pair parsing reads the first line as a group code, and a BOM would glue itself to it — a DXF re-saved as UTF-8 in a text editor is exactly this module's use case. - tests: regression test for the MTEXT sequence with no header codepage, plus one for a header behind a BOM.
1 parent e6c80ce commit 7142faf

2 files changed

Lines changed: 125 additions & 14 deletions

File tree

apps/geolibre-desktop/src/lib/cad-encoding.ts

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,18 @@ const CODEPAGE_LABELS: Record<string, string> = {
6060
*
6161
* The `latin1` label is windows-1252 in disguise (see {@link CODEPAGE_LABELS}),
6262
* which is harmless here: the header variables and values this scans are ASCII.
63+
* A leading UTF-8 BOM (`EF BB BF`, which that decode turns into ``) is
64+
* dropped so it cannot glue itself to the file's first group code — a DXF
65+
* re-saved as UTF-8 by a text editor is exactly the case this module exists for.
6366
*
6467
* @param bytes The DXF file bytes.
6568
* @returns The first {@link HEADER_PROBE_BYTES} decoded as Latin-1.
6669
*/
6770
function headerLatin1(bytes: Uint8Array): string {
68-
return new TextDecoder("latin1").decode(
71+
const text = new TextDecoder("latin1").decode(
6972
bytes.subarray(0, Math.min(bytes.length, HEADER_PROBE_BYTES)),
7073
);
74+
return text.replace(/^(?:\uFEFF|)/, "");
7175
}
7276

7377
/**
@@ -84,21 +88,87 @@ function isBinaryDxf(bytes: Uint8Array): boolean {
8488
return true;
8589
}
8690

91+
/** One HEADER variable: its value records, keyed by DXF group code. */
92+
type DxfHeaderVariable = Map<number, string>;
93+
8794
/**
88-
* Read one HEADER variable from an ASCII DXF prefix.
95+
* Parse the HEADER variables out of an ASCII DXF prefix.
96+
*
97+
* DXF is a flat stream of (group code, value) line pairs. A HEADER variable is
98+
* introduced by a group-9 record naming it, followed by the value records that
99+
* belong to it. Walking those pairs — rather than pattern-matching the text —
100+
* is what keeps an entity from impersonating a variable: an `MTEXT` whose own
101+
* content reads `$DWGCODEPAGE` sits under group 1, not group 9, and the scan
102+
* stops at the HEADER section's `ENDSEC` before reaching ENTITIES anyway.
103+
*
104+
* A pair whose group code is not a number is skipped rather than treated as a
105+
* value; pair alignment is fixed by the format, so one unreadable code does not
106+
* desync the ones after it.
89107
*
90108
* @param header Latin-1 text of the file prefix.
109+
* @returns Each variable name (uppercased, without `$`) mapped to its records.
110+
*/
111+
function readDxfHeaderVariables(header: string): Map<string, DxfHeaderVariable> {
112+
const lines = header.split(/\r?\n/);
113+
const variables = new Map<string, DxfHeaderVariable>();
114+
let awaitingSectionName = false;
115+
let inHeader = false;
116+
let current: DxfHeaderVariable | null = null;
117+
118+
for (let i = 0; i + 1 < lines.length; i += 2) {
119+
const code = Number.parseInt(lines[i]!.trim(), 10);
120+
const value = lines[i + 1]!.trim();
121+
if (!Number.isInteger(code)) continue;
122+
123+
if (code === 0 && value.toUpperCase() === "SECTION") {
124+
awaitingSectionName = true;
125+
inHeader = false;
126+
current = null;
127+
continue;
128+
}
129+
if (awaitingSectionName) {
130+
// The `2` record right after `0/SECTION` names the section.
131+
if (code === 2) {
132+
inHeader = value.toUpperCase() === "HEADER";
133+
awaitingSectionName = false;
134+
}
135+
continue;
136+
}
137+
if (!inHeader) continue;
138+
// HEADER is the first section, so its end is the end of anything readable.
139+
if (code === 0 && value.toUpperCase() === "ENDSEC") break;
140+
141+
if (code === 9) {
142+
const name = value.startsWith("$") ? value.slice(1).toUpperCase() : "";
143+
if (!name) {
144+
current = null;
145+
continue;
146+
}
147+
current = variables.get(name) ?? new Map<number, string>();
148+
variables.set(name, current);
149+
continue;
150+
}
151+
// First record of a given code wins, so a repeated variable cannot shadow
152+
// the value AutoCAD wrote first.
153+
if (current && !current.has(code)) current.set(code, value);
154+
}
155+
return variables;
156+
}
157+
158+
/**
159+
* Read one HEADER variable's value record.
160+
*
161+
* @param variables From {@link readDxfHeaderVariables}.
91162
* @param name The variable without `$`, such as `ACADVER`.
92-
* @param groupCode The DXF group code of the value line (`1` or `3`).
93-
* @returns The trimmed value, or null when the variable is absent.
163+
* @param groupCode The DXF group code of the value record (`1` or `3`).
164+
* @returns The trimmed value, or null when the variable or record is absent.
94165
*/
95-
function readDxfTaggedValue(header: string, name: string, groupCode: number): string | null {
96-
const pattern = new RegExp(
97-
`\\$${name}[ \\t]*\\r?\\n[ \\t]*0*${groupCode}[ \\t]*\\r?\\n([^\\r\\n]+)`,
98-
"i",
99-
);
100-
const value = pattern.exec(header)?.[1]?.trim();
101-
return value || null;
166+
function readDxfHeaderValue(
167+
variables: Map<string, DxfHeaderVariable>,
168+
name: string,
169+
groupCode: number,
170+
): string | null {
171+
return variables.get(name)?.get(groupCode) || null;
102172
}
103173

104174
/**
@@ -152,10 +222,10 @@ function normalizeCodepageToken(raw: string): string {
152222
*/
153223
export function readDxfCodepage(bytes: Uint8Array): string | null {
154224
if (bytes.length === 0 || isBinaryDxf(bytes)) return null;
155-
const header = headerLatin1(bytes);
156-
const acadver = readDxfTaggedValue(header, "ACADVER", 1);
225+
const variables = readDxfHeaderVariables(headerLatin1(bytes));
226+
const acadver = readDxfHeaderValue(variables, "ACADVER", 1);
157227
if (acadver && isUtf8DxfVersion(acadver)) return "UTF-8";
158-
const codepage = readDxfTaggedValue(header, "DWGCODEPAGE", 3);
228+
const codepage = readDxfHeaderValue(variables, "DWGCODEPAGE", 3);
159229
return codepage ? normalizeCodepageToken(codepage) : null;
160230
}
161231

tests/cad-encoding.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,47 @@ describe("readDxfCodepage", () => {
137137
assert.equal(readDxfCodepage(utf8Bytes(" 0\nSECTION\n 0\nEOF\n")), null);
138138
});
139139

140+
it("ignores an MTEXT entity whose own content reads $DWGCODEPAGE", () => {
141+
// The variable name only counts under group code 9, inside HEADER. Here it
142+
// is group-1 entity text in ENTITIES, so the drawing stays unlabelled.
143+
const bytes = utf8Bytes(
144+
[
145+
" 0",
146+
"SECTION",
147+
" 2",
148+
"HEADER",
149+
" 9",
150+
"$ACADVER",
151+
" 1",
152+
"AC1018",
153+
" 0",
154+
"ENDSEC",
155+
" 0",
156+
"SECTION",
157+
" 2",
158+
"ENTITIES",
159+
" 0",
160+
"MTEXT",
161+
" 1",
162+
"$DWGCODEPAGE",
163+
" 3",
164+
"ANSI_936",
165+
" 0",
166+
"ENDSEC",
167+
" 0",
168+
"EOF",
169+
"",
170+
].join("\n"),
171+
);
172+
assert.equal(readDxfCodepage(bytes), null);
173+
});
174+
175+
it("reads a header behind a UTF-8 BOM", () => {
176+
const bom = Uint8Array.from([0xef, 0xbb, 0xbf]);
177+
const bytes = concatBytes([bom, dxfWithText("ANSI_936", utf8Bytes("x"))]);
178+
assert.equal(readDxfCodepage(bytes), "ANSI_936");
179+
});
180+
140181
it("accepts unpadded and zero-padded HEADER group codes", () => {
141182
const unpadded = [
142183
"0",

0 commit comments

Comments
 (0)