Skip to content

Commit fa8c2a4

Browse files
committed
test(oc-path): cover utf16 error truncation
1 parent b203180 commit fa8c2a4

2 files changed

Lines changed: 38 additions & 1 deletion

File tree

extensions/oc-path/src/oc-path/oc-path.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* @module @openclaw/oc-path/oc-path
1111
*/
1212

13-
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
13+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
1414
import { OcEmitSentinelError, REDACTED_SENTINEL } from "./sentinel.js";
1515

1616
const OC_SCHEME = "oc://";

extensions/oc-path/src/oc-path/tests/scenarios/security-and-limits.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ import {
1313
import { parseJsonc } from "../../jsonc/parse.js";
1414
import { parseJsonl } from "../../jsonl/parse.js";
1515

16+
function expectUtf16SafeLimitError(run: () => unknown, expectedInput: string): void {
17+
try {
18+
run();
19+
expect.fail("expected an OC_PATH_TOO_LONG error");
20+
} catch (err) {
21+
expect(err).toBeInstanceOf(OcPathError);
22+
expect((err as OcPathError).code).toBe("OC_PATH_TOO_LONG");
23+
expect((err as OcPathError).input).toBe(expectedInput);
24+
}
25+
}
26+
1627
describe("encoding edges", () => {
1728
it("strips leading UTF-8 BOM from path string", () => {
1829
expect(parseOcPath("oc://X/Y").file).toBe("X");
@@ -77,6 +88,23 @@ describe("path-string and traversal caps", () => {
7788
expect(() => parseOcPath("oc://X/" + "a".repeat(MAX_PATH_LENGTH))).toThrow(/exceeds .* bytes/);
7889
});
7990

91+
it("keeps overlong parse input UTF-16 safe", () => {
92+
const prefix = `oc://${"a".repeat(74)}`;
93+
expectUtf16SafeLimitError(
94+
() => parseOcPath(`${prefix}😀${"b".repeat(MAX_PATH_LENGTH)}`),
95+
`${prefix}…`,
96+
);
97+
});
98+
99+
it("keeps post-NFC overlong parse input UTF-16 safe", () => {
100+
const prefix = `oc://${"a".repeat(74)}`;
101+
const input = `${prefix}😀${"\u0344".repeat(MAX_PATH_LENGTH - prefix.length - 2)}`;
102+
expect(input).toHaveLength(MAX_PATH_LENGTH);
103+
expect(input.normalize("NFC").length).toBeGreaterThan(MAX_PATH_LENGTH);
104+
105+
expectUtf16SafeLimitError(() => parseOcPath(input), `${prefix}…`);
106+
});
107+
80108
it("parseOcPath accepts a path right at the cap", () => {
81109
const justUnder = "oc://X/" + "a".repeat(MAX_PATH_LENGTH - "oc://X/".length);
82110
expect(() => parseOcPath(justUnder)).not.toThrow();
@@ -88,6 +116,15 @@ describe("path-string and traversal caps", () => {
88116
);
89117
});
90118

119+
it("keeps overlong formatted paths UTF-16 safe", () => {
120+
const sectionPrefix = "a".repeat(72);
121+
expectUtf16SafeLimitError(
122+
() =>
123+
formatOcPath({ file: "X", section: `${sectionPrefix}😀${"b".repeat(MAX_PATH_LENGTH)}` }),
124+
`oc://X/${sectionPrefix}…`,
125+
);
126+
});
127+
91128
it("walker depth cap fires on synthetic deeply-nested AST", () => {
92129
// Bypasses parser depth cap so the walker defense fires in isolation.
93130
type V = import("../../jsonc/ast.js").JsoncValue;

0 commit comments

Comments
 (0)