From 512b8c05da4d7db8108a3ea9234f09307d35ed15 Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Wed, 23 Sep 2026 15:29:23 +0000 Subject: [PATCH v2] Copy whole repetitions of the pad string at once in lpad() and rpad() The padding loop called pg_mblen_range() and memcpy() once per character. Walk the pad string once to count its characters, copy the whole repetitions as byte sequences by doubling the copied region, and use the per-character loop only for the partial final repetition. The pad string is validated as far as before, which is all of it when a full repetition is copied and otherwise only the characters copied. --- src/backend/utils/adt/oracle_compat.c | 101 +++++++++++++++++-------- src/test/regress/expected/encoding.out | 37 +++++++++ src/test/regress/expected/strings.out | 26 +++++++ src/test/regress/sql/encoding.sql | 10 +++ src/test/regress/sql/strings.sql | 7 ++ 5 files changed, 150 insertions(+), 31 deletions(-) diff --git a/src/backend/utils/adt/oracle_compat.c b/src/backend/utils/adt/oracle_compat.c index 7422a454397..c2c58f28e38 100644 --- a/src/backend/utils/adt/oracle_compat.c +++ b/src/backend/utils/adt/oracle_compat.c @@ -143,6 +143,74 @@ casefold(PG_FUNCTION_ARGS) } +/* + * Write m characters of padding at dst, taken cyclically from the pad + * string of padlen bytes, and return the number of bytes written. + * + * The pad string is validated with pg_mblen_range() only as far as it is + * used, so an incomplete multibyte character at its end is an error only + * if the padding reaches it. + */ +static int +pad_fill(char *dst, const char *pad, int padlen, int m) +{ + const char *padend = pad + padlen; + const char *p = pad; + int nchars = 0; + int nrep; + int total; + int copied; + + if (m <= 0) + return 0; + + /* count the characters of one repetition, stopping at m */ + while (p < padend && nchars < m) + { + p += pg_mblen_range(p, padend); + nchars++; + } + + /* fewer than one repetition is needed, so copy the first m characters */ + if (p < padend) + { + memcpy(dst, pad, p - pad); + return p - pad; + } + + /* + * Whole repetitions are byte copies of the pad string. Copy one, then + * double the copied region until all of them are written, so the number + * of memcpy() calls is logarithmic in the number of repetitions. total + * cannot overflow, since the caller sized the output for m characters. + */ + nrep = m / nchars; + total = nrep * padlen; + memcpy(dst, pad, padlen); + copied = padlen; + while (copied < total) + { + int n = Min(copied, total - copied); + + memcpy(dst + copied, dst, n); + copied += n; + } + + /* partial final repetition, one character at a time */ + m -= nrep * nchars; + p = pad; + while (m-- > 0) + { + int mlen = pg_mblen_range(p, padend); + + memcpy(dst + copied, p, mlen); + copied += mlen; + p += mlen; + } + + return copied; +} + /******************************************************************** * * lpad @@ -167,10 +235,7 @@ lpad(PG_FUNCTION_ARGS) text *string2 = PG_GETARG_TEXT_PP(2); text *ret; char *ptr1, - *ptr2, - *ptr2start, *ptr_ret; - const char *ptr2end; int m, s1len, s2len; @@ -209,20 +274,9 @@ lpad(PG_FUNCTION_ARGS) m = len - s1len; - ptr2 = ptr2start = VARDATA_ANY(string2); - ptr2end = ptr2 + s2len; ptr_ret = VARDATA(ret); - while (m--) - { - int mlen = pg_mblen_range(ptr2, ptr2end); - - memcpy(ptr_ret, ptr2, mlen); - ptr_ret += mlen; - ptr2 += mlen; - if (ptr2 == ptr2end) /* wrap around at end of s2 */ - ptr2 = ptr2start; - } + ptr_ret += pad_fill(ptr_ret, VARDATA_ANY(string2), s2len, m); ptr1 = VARDATA_ANY(string1); @@ -265,10 +319,7 @@ rpad(PG_FUNCTION_ARGS) text *string2 = PG_GETARG_TEXT_PP(2); text *ret; char *ptr1, - *ptr2, - *ptr2start, *ptr_ret; - const char *ptr2end; int m, s1len, s2len; @@ -320,19 +371,7 @@ rpad(PG_FUNCTION_ARGS) ptr1 += mlen; } - ptr2 = ptr2start = VARDATA_ANY(string2); - ptr2end = ptr2 + s2len; - - while (m--) - { - int mlen = pg_mblen_range(ptr2, ptr2end); - - memcpy(ptr_ret, ptr2, mlen); - ptr_ret += mlen; - ptr2 += mlen; - if (ptr2 == ptr2end) /* wrap around at end of s2 */ - ptr2 = ptr2start; - } + ptr_ret += pad_fill(ptr_ret, VARDATA_ANY(string2), s2len, m); SET_VARSIZE(ret, ptr_ret - (char *) ret); diff --git a/src/test/regress/expected/encoding.out b/src/test/regress/expected/encoding.out index 0bb72a1df6f..9fc871215b0 100644 --- a/src/test/regress/expected/encoding.out +++ b/src/test/regress/expected/encoding.out @@ -60,6 +60,43 @@ SELECT reverse(good) FROM regress_encoding; éfac (1 row) +-- multibyte pad strings: whole repetitions, a partial final repetition, and +-- fewer than one repetition +SELECT lpad(good, 7, 'é'), rpad(good, 7, 'é') FROM regress_encoding; + lpad | rpad +---------+--------- + ééécafé | caféééé +(1 row) + +SELECT lpad(good, 12, 'éab'), rpad(good, 12, 'éab') FROM regress_encoding; + lpad | rpad +--------------+-------------- + éabéabéacafé | cafééabéabéa +(1 row) + +SELECT lpad(good, 5, 'éab'), rpad(good, 5, 'éab') FROM regress_encoding; + lpad | rpad +-------+------- + écafé | caféé +(1 row) + +-- a lone lead byte in the pad string is an error if the padding reaches it +SELECT lpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 +SELECT rpad(good, 7, 'ab' || test_bytea_to_text('\xc3')) FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 +SELECT lpad(good, 5, 'ab' || test_bytea_to_text('\xc3')), rpad(good, 6, 'ab' || test_bytea_to_text('\xc3')) FROM regress_encoding; + lpad | rpad +-------+-------- + acafé | caféab +(1 row) + +SELECT lpad(good, 4, test_bytea_to_text('\xc3')), rpad(good, 4, test_bytea_to_text('\xc3')) FROM regress_encoding; + lpad | rpad +------+------ + café | café +(1 row) + -- invalid short mb character = error SELECT length(truncated) FROM regress_encoding; ERROR: invalid byte sequence for encoding "UTF8": 0xc3 diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out index fa29abfd829..6e064e3807f 100644 --- a/src/test/regress/expected/strings.out +++ b/src/test/regress/expected/strings.out @@ -3441,6 +3441,32 @@ SELECT rpad('hi', 5, ''); hi (1 row) +-- whole repetitions of the pad string, a partial final repetition, and +-- fewer than one repetition +SELECT lpad('hi', 8, 'abc'), rpad('hi', 8, 'abc'); + lpad | rpad +----------+---------- + abcabchi | hiabcabc +(1 row) + +SELECT lpad('hi', 9, 'abc'), rpad('hi', 9, 'abc'); + lpad | rpad +-----------+----------- + abcabcahi | hiabcabca +(1 row) + +SELECT lpad('hi', 12, 'ab'), rpad('hi', 12, 'ab'); + lpad | rpad +--------------+-------------- + abababababhi | hiababababab +(1 row) + +SELECT lpad('hi', 3, 'abc'), rpad('hi', 3, 'abc'); + lpad | rpad +------+------ + ahi | hia +(1 row) + SELECT ltrim('zzzytrim', 'xyz'); ltrim ------- diff --git a/src/test/regress/sql/encoding.sql b/src/test/regress/sql/encoding.sql index 26caa93a5d5..3ea6e54e52d 100644 --- a/src/test/regress/sql/encoding.sql +++ b/src/test/regress/sql/encoding.sql @@ -37,6 +37,16 @@ SELECT substring(good, 3, 1) FROM regress_encoding; SELECT substring(good, 4, 1) FROM regress_encoding; SELECT regexp_replace(good, '^caf(.)$', '\1') FROM regress_encoding; SELECT reverse(good) FROM regress_encoding; +-- multibyte pad strings: whole repetitions, a partial final repetition, and +-- fewer than one repetition +SELECT lpad(good, 7, 'é'), rpad(good, 7, 'é') FROM regress_encoding; +SELECT lpad(good, 12, 'éab'), rpad(good, 12, 'éab') FROM regress_encoding; +SELECT lpad(good, 5, 'éab'), rpad(good, 5, 'éab') FROM regress_encoding; +-- a lone lead byte in the pad string is an error if the padding reaches it +SELECT lpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding; +SELECT rpad(good, 7, 'ab' || test_bytea_to_text('\xc3')) FROM regress_encoding; +SELECT lpad(good, 5, 'ab' || test_bytea_to_text('\xc3')), rpad(good, 6, 'ab' || test_bytea_to_text('\xc3')) FROM regress_encoding; +SELECT lpad(good, 4, test_bytea_to_text('\xc3')), rpad(good, 4, test_bytea_to_text('\xc3')) FROM regress_encoding; -- invalid short mb character = error SELECT length(truncated) FROM regress_encoding; diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql index 7d9c7275a02..04651a0a46f 100644 --- a/src/test/regress/sql/strings.sql +++ b/src/test/regress/sql/strings.sql @@ -1165,6 +1165,13 @@ SELECT rpad('hi', -5, 'xy'); SELECT rpad('hello', 2); SELECT rpad('hi', 5, ''); +-- whole repetitions of the pad string, a partial final repetition, and +-- fewer than one repetition +SELECT lpad('hi', 8, 'abc'), rpad('hi', 8, 'abc'); +SELECT lpad('hi', 9, 'abc'), rpad('hi', 9, 'abc'); +SELECT lpad('hi', 12, 'ab'), rpad('hi', 12, 'ab'); +SELECT lpad('hi', 3, 'abc'), rpad('hi', 3, 'abc'); + SELECT ltrim('zzzytrim', 'xyz'); SELECT translate('', '14', 'ax'); -- 2.43.0