mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
fix(oc-path): use truncateUtf16Safe for error message path truncation (#102527)
* fix(oc-path): use truncateUtf16Safe for error message path truncation Replace naive .slice(0, 80) with truncateUtf16Safe() at three error-reporting call sites to prevent surrogate pair splitting in oc:// path validation error messages. Co-Authored-By: Claude <noreply@anthropic.com> * test(oc-path): cover utf16 error truncation --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
parent
ed9ce1c584
commit
145b71f57e
2 changed files with 41 additions and 3 deletions
|
|
@ -10,6 +10,7 @@
|
|||
* @module @openclaw/oc-path/oc-path
|
||||
*/
|
||||
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { OcEmitSentinelError, REDACTED_SENTINEL } from "./sentinel.js";
|
||||
|
||||
const OC_SCHEME = "oc://";
|
||||
|
|
@ -134,7 +135,7 @@ export function parseOcPath(input: string): OcPath {
|
|||
if (input.length > MAX_PATH_LENGTH) {
|
||||
fail(
|
||||
`oc:// path exceeds ${MAX_PATH_LENGTH} bytes (length: ${input.length})`,
|
||||
input.slice(0, 80) + "…",
|
||||
truncateUtf16Safe(input, 80) + "…",
|
||||
"OC_PATH_TOO_LONG",
|
||||
);
|
||||
}
|
||||
|
|
@ -147,7 +148,7 @@ export function parseOcPath(input: string): OcPath {
|
|||
if (normalized.length > MAX_PATH_LENGTH) {
|
||||
fail(
|
||||
`oc:// path exceeds ${MAX_PATH_LENGTH} bytes after NFC (length: ${normalized.length})`,
|
||||
input.slice(0, 80) + "…",
|
||||
truncateUtf16Safe(input, 80) + "…",
|
||||
"OC_PATH_TOO_LONG",
|
||||
);
|
||||
}
|
||||
|
|
@ -311,7 +312,7 @@ export function formatOcPath(path: OcPath): string {
|
|||
if (out.length > MAX_PATH_LENGTH) {
|
||||
fail(
|
||||
`Formatted oc:// exceeds ${MAX_PATH_LENGTH} bytes (length: ${out.length})`,
|
||||
out.slice(0, 80) + "…",
|
||||
truncateUtf16Safe(out, 80) + "…",
|
||||
"OC_PATH_TOO_LONG",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,17 @@ import {
|
|||
import { parseJsonc } from "../../jsonc/parse.js";
|
||||
import { parseJsonl } from "../../jsonl/parse.js";
|
||||
|
||||
function expectUtf16SafeLimitError(run: () => unknown, expectedInput: string): void {
|
||||
try {
|
||||
run();
|
||||
expect.fail("expected an OC_PATH_TOO_LONG error");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(OcPathError);
|
||||
expect((err as OcPathError).code).toBe("OC_PATH_TOO_LONG");
|
||||
expect((err as OcPathError).input).toBe(expectedInput);
|
||||
}
|
||||
}
|
||||
|
||||
describe("encoding edges", () => {
|
||||
it("strips leading UTF-8 BOM from path string", () => {
|
||||
expect(parseOcPath("oc://X/Y").file).toBe("X");
|
||||
|
|
@ -77,6 +88,23 @@ describe("path-string and traversal caps", () => {
|
|||
expect(() => parseOcPath("oc://X/" + "a".repeat(MAX_PATH_LENGTH))).toThrow(/exceeds .* bytes/);
|
||||
});
|
||||
|
||||
it("keeps overlong parse input UTF-16 safe", () => {
|
||||
const prefix = `oc://${"a".repeat(74)}`;
|
||||
expectUtf16SafeLimitError(
|
||||
() => parseOcPath(`${prefix}😀${"b".repeat(MAX_PATH_LENGTH)}`),
|
||||
`${prefix}…`,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps post-NFC overlong parse input UTF-16 safe", () => {
|
||||
const prefix = `oc://${"a".repeat(74)}`;
|
||||
const input = `${prefix}😀${"\u0344".repeat(MAX_PATH_LENGTH - prefix.length - 2)}`;
|
||||
expect(input).toHaveLength(MAX_PATH_LENGTH);
|
||||
expect(input.normalize("NFC").length).toBeGreaterThan(MAX_PATH_LENGTH);
|
||||
|
||||
expectUtf16SafeLimitError(() => parseOcPath(input), `${prefix}…`);
|
||||
});
|
||||
|
||||
it("parseOcPath accepts a path right at the cap", () => {
|
||||
const justUnder = "oc://X/" + "a".repeat(MAX_PATH_LENGTH - "oc://X/".length);
|
||||
expect(() => parseOcPath(justUnder)).not.toThrow();
|
||||
|
|
@ -88,6 +116,15 @@ describe("path-string and traversal caps", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("keeps overlong formatted paths UTF-16 safe", () => {
|
||||
const sectionPrefix = "a".repeat(72);
|
||||
expectUtf16SafeLimitError(
|
||||
() =>
|
||||
formatOcPath({ file: "X", section: `${sectionPrefix}😀${"b".repeat(MAX_PATH_LENGTH)}` }),
|
||||
`oc://X/${sectionPrefix}…`,
|
||||
);
|
||||
});
|
||||
|
||||
it("walker depth cap fires on synthetic deeply-nested AST", () => {
|
||||
// Bypasses parser depth cap so the walker defense fires in isolation.
|
||||
type V = import("../../jsonc/ast.js").JsoncValue;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue