mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Pad ASCII strings without indexing their UTF-16 units (#2392)
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/pad-case-converted-strings.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -2161,6 +2161,7 @@ static ScrStr *scr_str_case_conv(const ScrStr *s, int to_lower) {
|
||||
out->data[i] = (char)(c >= first && c <= last ? c ^ 0x20 : c);
|
||||
}
|
||||
out->data[s->len] = '\0';
|
||||
scr_str_note_ascii(out);
|
||||
return out;
|
||||
}
|
||||
ScrJsonBuf b;
|
||||
|
||||
@@ -2118,6 +2118,14 @@ double scr_str_utf16_len(ScrStr *s) {
|
||||
return (double)(int64_t)units;
|
||||
}
|
||||
|
||||
/* scr_str_utf16_len as a size: proven-ASCII and short strings are counted
|
||||
* here, without the call. */
|
||||
static inline size_t scr_str_len16(ScrStr *s) {
|
||||
if (s->ascii) return s->len;
|
||||
if (s->len <= 64) return scr_utf16_units_short(s->data, s->len);
|
||||
return (size_t)scr_str_utf16_len(s);
|
||||
}
|
||||
|
||||
/* Records the UTF-16 length of a string whose producer counted it (latin1
|
||||
* text has one unit per byte), so scr_str_utf16_len does not walk it. Only
|
||||
* strings that scr_str_utf16_len would otherwise record are kept. */
|
||||
@@ -3137,15 +3145,17 @@ ScrStr *scr_str_replace_literal(ScrStr *s, ScrStr *search, ScrStr *rep, bool all
|
||||
static ScrStr *scr_pad_impl(ScrStr *s, double maxLength, ScrStr *fill,
|
||||
bool at_start) {
|
||||
double target = scr_to_integer_or_infinity(maxLength);
|
||||
size_t len16 = scr_sidx_len(s, scr_sidx_for(s));
|
||||
size_t len16 = scr_str_len16(s);
|
||||
if (!(target > (double)len16) || fill->len == 0) return scr_str_retain(s);
|
||||
/* Reject pad sizes malloc could not satisfy before the double→size_t
|
||||
* conversion can overflow (each unit is at most 3 bytes here: BMP chars
|
||||
* and lone surrogates; astral chars are 4 bytes for 2 units). */
|
||||
if (target > (double)((SIZE_MAX - sizeof(ScrStr) - 1) / 4)) scr_oom();
|
||||
size_t pad16 = (size_t)target - len16;
|
||||
size_t fill16 = scr_sidx_len(fill, scr_sidx_for(fill));
|
||||
size_t reps = pad16 / fill16, rem16 = pad16 % fill16;
|
||||
size_t fill16 = scr_str_len16(fill);
|
||||
/* A one-unit fill, the common case, needs no division. */
|
||||
size_t reps = fill16 == 1 ? pad16 : pad16 / fill16;
|
||||
size_t rem16 = fill16 == 1 ? 0 : pad16 % fill16;
|
||||
/* The truncated prefix of fill: whole chars while they fit in rem16
|
||||
* units; a final astral char that doesn't fit contributes its high
|
||||
* surrogate. */
|
||||
@@ -3167,9 +3177,14 @@ static ScrStr *scr_pad_impl(ScrStr *s, double maxLength, ScrStr *fill,
|
||||
size_t total = s->len + pad_b;
|
||||
ScrStr *r = scr_str_alloc(total, total);
|
||||
char *w = r->data + (at_start ? 0 : s->len);
|
||||
for (size_t i = 0; i < reps; i++) {
|
||||
memcpy(w, fill->data, fill->len);
|
||||
w += fill->len;
|
||||
if (fill->len == 1) {
|
||||
memset(w, fill->data[0], reps);
|
||||
w += reps;
|
||||
} else {
|
||||
for (size_t i = 0; i < reps; i++) {
|
||||
memcpy(w, fill->data, fill->len);
|
||||
w += fill->len;
|
||||
}
|
||||
}
|
||||
memcpy(w, fill->data, prefix_b);
|
||||
w += prefix_b;
|
||||
@@ -3179,6 +3194,7 @@ static ScrStr *scr_pad_impl(ScrStr *s, double maxLength, ScrStr *fill,
|
||||
}
|
||||
memcpy(at_start ? w : r->data, s->data, s->len);
|
||||
r->data[total] = '\0';
|
||||
if (s->ascii && fill->ascii) scr_str_note_ascii(r);
|
||||
/* Only a piece that starts with a lone low surrogate can complete a
|
||||
* pair across a seam: the fill (after a fill or the receiver) or the
|
||||
* receiver (after a fill ending in a high surrogate). */
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// padStart and padEnd on the results of case conversion and slicing, with
|
||||
// ASCII and non-ASCII receivers and fills, and index reads of the results.
|
||||
|
||||
const words = ["alpha", "beta", "gamma", "Delta", "épée", "straße", "日本語", "a😀b"];
|
||||
const fills = ["-", "ab", "é", "😀", "", " "];
|
||||
|
||||
function describe(s: string): string {
|
||||
return [
|
||||
JSON.stringify(s),
|
||||
s.length,
|
||||
s.charAt(1),
|
||||
s.at(-1),
|
||||
s.indexOf("A"),
|
||||
s.slice(1, 4),
|
||||
s.charCodeAt(s.length - 1),
|
||||
s.codePointAt(0),
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
for (const word of words) {
|
||||
const upper = word.slice(0, 3).toUpperCase();
|
||||
const lower = word.toLowerCase();
|
||||
for (const fill of fills) {
|
||||
console.log(describe(upper.padStart(6, fill)));
|
||||
console.log(describe(lower.padEnd(9, fill)));
|
||||
}
|
||||
console.log(describe(upper.padStart(2)), describe(upper.padEnd(5)));
|
||||
}
|
||||
|
||||
// The same string padded again, compared and used as a key.
|
||||
const counts = new Map<string, number>();
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const key = words[i % words.length]!.slice(0, 3).toUpperCase().padStart(4, "-");
|
||||
counts.set(key, (counts.get(key) ?? 0) + 1);
|
||||
}
|
||||
console.log([...counts].map(([k, v]) => `${k}=${v}`).join(","));
|
||||
const padded = "abc".toUpperCase().padStart(5, "😀");
|
||||
console.log(padded.length, [...padded].length, padded === "\ud83dABC".padStart(5, "😀"));
|
||||
console.log("x".toUpperCase().padEnd(4, "é").normalize("NFD").length);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3600, "san": 3700 }
|
||||
Reference in New Issue
Block a user