Skip to content

Commit 7155816

Browse files
MingMing
authored andcommitted
msteams: repair bare JSON escapes robustly
1 parent 1d55234 commit 7155816

2 files changed

Lines changed: 59 additions & 4 deletions

File tree

extensions/msteams/src/__tests__/monitor-json-repair.test.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,39 @@
1414

1515
import { describe, it, expect } from "vitest";
1616

17-
/** Mirrors the repair regex from monitor.ts middleware */
18-
const REPAIR_REGEX = /\\([^"\\\//bfnrtu])/g;
17+
// ── Mirror `repairJsonEscapes` from monitor.ts ─────────────────────────────
18+
const VALID_JSON_ESCAPE_CHARS = new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
1919

20-
/** Emulates the two-pass parse logic from the fixed middleware */
20+
function repairJsonEscapes(raw: string): string {
21+
let fixed = "";
22+
for (let i = 0; i < raw.length; i += 1) {
23+
const ch = raw[i];
24+
if (ch !== "\\") {
25+
fixed += ch;
26+
continue;
27+
}
28+
29+
let runEnd = i;
30+
while (raw[runEnd] === "\\") {
31+
runEnd += 1;
32+
}
33+
const runLength = runEnd - i;
34+
fixed += "\\".repeat(runLength);
35+
const next = raw[runEnd];
36+
if (runLength % 2 === 1 && next !== undefined && !VALID_JSON_ESCAPE_CHARS.has(next)) {
37+
fixed += "\\";
38+
}
39+
i = runEnd - 1;
40+
}
41+
return fixed;
42+
}
43+
44+
/** Emulate the two-pass middleware. Returns parsed body + which pass was used. */
2145
function twoPassParse(raw: string): { body: unknown; path: "first_pass" | "repaired" } {
2246
try {
2347
return { body: JSON.parse(raw), path: "first_pass" };
2448
} catch {
25-
const fixed = raw.replace(REPAIR_REGEX, "\\\\$1");
49+
const fixed = repairJsonEscapes(raw);
2650
return { body: JSON.parse(fixed), path: "repaired" }; // throws if still invalid
2751
}
2852
}
@@ -62,6 +86,15 @@ const GOOD_MESSAGE_PAYLOAD = JSON.stringify({
6286
/** Payload with valid JSON escape sequences — must NOT be double-escaped */
6387
const VALID_ESCAPES_PAYLOAD = String.raw`{"text": "line1\nline2\ttab\"quote\\backslash"}`;
6488

89+
/**
90+
* The edge case: a valid \\q sequence (JSON for literal \q)
91+
* mixed with a separate invalid escape in the same payload.
92+
*/
93+
const MIXED_VALID_AND_INVALID_ESCAPES = String.raw`{
94+
"path": "C:\\q",
95+
"team": "Test\Project"
96+
}`;
97+
6598
// ────────────────────────────────────────────────────────────
6699
// Tests
67100
// ────────────────────────────────────────────────────────────
@@ -101,4 +134,12 @@ describe("msteams JSON repair middleware", () => {
101134
const garbage = "{ not valid json at all ??? }";
102135
expect(() => twoPassParse(garbage)).toThrow(SyntaxError);
103136
});
137+
138+
it("preserves valid escaped backslashes while repairing another field", () => {
139+
const { body, path } = twoPassParse(MIXED_VALID_AND_INVALID_ESCAPES);
140+
expect(path).toBe("repaired");
141+
const obj = body as Record<string, string>;
142+
expect(obj["path"]).toBe("C:\\q");
143+
expect(obj["team"]).toBe("Test\\Project");
144+
});
104145
});

extensions/msteams/src/monitor.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ export type MonitorMSTeamsResult = {
4444

4545
const MSTEAMS_WEBHOOK_MAX_BODY_BYTES = DEFAULT_WEBHOOK_MAX_BODY_BYTES;
4646

47+
/**
48+
* Attempt to repair a JSON string that contains invalid bare-backslash escape
49+
* sequences such as \p, \q, \c (not defined by RFC 8259).
50+
*
51+
* The repair walks each consecutive run of backslashes. Even-length runs are
52+
* already made of valid `\\` pairs. Odd-length runs contain one trailing escape
53+
* opener; when that opener is followed by a non-JSON escape character, we add
54+
* one backslash so the character is preserved literally.
55+
*
56+
* Examples:
57+
* "\\q" (valid JSON: literal \q) → unchanged
58+
* "\q" (invalid JSON: bare \q) → "\\q"
59+
* "\\\q" (valid \\ + invalid \q) → "\\\\q"
60+
*/
4761
const VALID_JSON_ESCAPE_CHARS = new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
4862

4963
function repairJsonEscapes(raw: string): string {

0 commit comments

Comments
 (0)