What happened
sliceUtf16Safe documents itself as a surrogate-safe slice, but when end < start it silently swaps the bounds and returns the middle of the string, whereas String.prototype.slice (which it otherwise mirrors, including negative-index handling) returns "" in that case.
Code:
|
/** Slices a UTF-16 string without returning dangling surrogate halves at either edge. */ |
|
export function sliceUtf16Safe(input: string, start: number, end?: number): string { |
|
const len = input.length; |
|
|
|
let from = start < 0 ? Math.max(len + start, 0) : Math.min(start, len); |
|
let to = end === undefined ? len : end < 0 ? Math.max(len + end, 0) : Math.min(end, len); |
|
|
|
if (to < from) { |
|
const tmp = from; |
|
from = to; |
|
to = tmp; |
|
} |
|
|
|
if (from > 0 && from < len) { |
|
const codeUnit = input.charCodeAt(from); |
|
if (isLowSurrogate(codeUnit) && isHighSurrogate(input.charCodeAt(from - 1))) { |
|
from += 1; |
|
} |
|
} |
|
|
|
if (to > 0 && to < len) { |
|
const codeUnit = input.charCodeAt(to - 1); |
|
if (isHighSurrogate(codeUnit) && isLowSurrogate(input.charCodeAt(to))) { |
|
to -= 1; |
|
} |
|
} |
|
|
|
return input.slice(from, to); |
|
} |
Repro
sliceUtf16Safe("hello", 4, 1); // "ell"
"hello".slice(4, 1); // ""
Expected
Match String.prototype.slice semantics (return "" when to <= from) so callers can substitute it for .slice safely — that drop-in substitution is its purpose (see truncateUtf16Safe in the same file, and re-exports from src/utils.ts).
Impact
Any caller that computes a start/end pair where the end can land before the start (e.g. index math around markers) gets an unexpected non-empty substring instead of "", which is a subtle logic-corruption footgun for a widely re-exported helper.
Environment
Current main (4287d26).
Found via source review (AI-assisted).
What happened
sliceUtf16Safedocuments itself as a surrogate-safe slice, but whenend < startit silently swaps the bounds and returns the middle of the string, whereasString.prototype.slice(which it otherwise mirrors, including negative-index handling) returns""in that case.Code:
openclaw/src/shared/utf16-slice.ts
Lines 15 to 43 in 4287d26
Repro
Expected
Match
String.prototype.slicesemantics (return ""whento <= from) so callers can substitute it for.slicesafely — that drop-in substitution is its purpose (seetruncateUtf16Safein the same file, and re-exports fromsrc/utils.ts).Impact
Any caller that computes a start/end pair where the end can land before the start (e.g. index math around markers) gets an unexpected non-empty substring instead of
"", which is a subtle logic-corruption footgun for a widely re-exported helper.Environment
Current
main(4287d26).Found via source review (AI-assisted).