Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ PHP NEWS
(Weilin Du)
. Added Transliterator::toRules() and transliterator_to_rules().
(Weilin Du, David Carlier)
. Added grapheme_str_pad(). (Chris Abner Herrera Navarro)
. Fixed Collator attribute and strength methods not rejecting an
unconstructed Collator. (Ilia Alshanetsky)
. Added optional $availableLocales argument to Locale::acceptFromHttp()
Expand Down
3 changes: 3 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ PHP 8.7 UPGRADE NOTES
. Added Transliterator::toRules() and transliterator_to_rules() to export
rules that can be used to recreate a transliterator. The optional
$escapeUnprintable argument escapes unprintable characters when true.
. Added grapheme_str_pad() to pad a string to a length counted in grapheme
clusters, the grapheme counterpart of str_pad() and mb_str_pad().
RFC: https://wiki.php.net/rfc/grapheme_str_pad

========================================
7. New Classes and Interfaces
Expand Down
217 changes: 217 additions & 0 deletions ext/intl/grapheme/grapheme_string.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#endif

#include <php.h>
#include "ext/standard/php_string.h"
#include "grapheme.h"
#include "grapheme_util.h"

Expand Down Expand Up @@ -1151,6 +1152,222 @@ U_CFUNC PHP_FUNCTION(grapheme_levenshtein)
efree(ustring1);
}

/* {{{ Returns the byte length, in the UTF-8 string str, of the first upos UTF-16 code units of ustr,
* its UTF-16 conversion. */
static size_t grapheme_str_pad_utf8_offset(const zend_string *str, const UChar *ustr, int32_t upos)
{
int32_t code_points = u_countChar32(ustr, upos);
int32_t offset = 0;
U8_FWD_N(ZSTR_VAL(str), offset, (int32_t) ZSTR_LEN(str), code_points);
return offset;
}
/* }}} */

/* {{{ Counts the grapheme clusters of a UTF-8 string, but stops once limit clusters have been counted.
* Also stores the number of bytes taken by its first prefix1 and prefix2 clusters in bytes1 and bytes2,
* if the string has that many (0 otherwise). Returns false on failure. */
static bool grapheme_str_pad_measure(const zend_string *str, size_t limit, size_t *count,
size_t prefix1, size_t *bytes1, size_t prefix2, size_t *bytes2)
{
if (grapheme_ascii_check((const unsigned char *) ZSTR_VAL(str), ZSTR_LEN(str)) >= 0) {
*count = ZSTR_LEN(str);
*bytes1 = prefix1 <= ZSTR_LEN(str) ? prefix1 : 0;
*bytes2 = prefix2 <= ZSTR_LEN(str) ? prefix2 : 0;
return true;
}

if (UNEXPECTED(ZSTR_LEN(str) > INT32_MAX)) {
intl_error_set_code(nullptr, U_INDEX_OUTOFBOUNDS_ERROR);
intl_error_set_custom_msg(nullptr, "String is too long");
return false;
}

/* Like grapheme_strlen(), work on UTF-16. The conversion also rejects invalid UTF-8. */
UErrorCode ustatus = U_ZERO_ERROR;
UChar *ustr = nullptr;
int32_t ulen = 0;
intl_convert_utf8_to_utf16(&ustr, &ulen, ZSTR_VAL(str), ZSTR_LEN(str), &ustatus);

if (U_FAILURE(ustatus)) {
intl_error_set_code(nullptr, ustatus);
intl_error_set_custom_msg(nullptr, "Invalid UTF-8 string");
if (ustr) {
efree(ustr);
}
return false;
}

UBreakIterator *bi = grapheme_get_break_iterator(&ustatus);

if (U_FAILURE(ustatus)) {
intl_error_set_code(nullptr, ustatus);
intl_error_set_custom_msg(nullptr, "Error creating grapheme break iterator");
efree(ustr);
return false;
}

ubrk_setText(bi, ustr, ulen, &ustatus);

if (U_FAILURE(ustatus)) {
intl_error_set_code(nullptr, ustatus);
intl_error_set_custom_msg(nullptr, "Error setting up grapheme break iterator");
ubrk_close(bi);
efree(ustr);
return false;
}

size_t graphemes = 0;
int32_t upos1 = 0, upos2 = 0;

for (int32_t pos = ubrk_next(bi); pos != UBRK_DONE && graphemes < limit; pos = ubrk_next(bi)) {
graphemes++;
if (graphemes == prefix1) {
upos1 = pos;
}
if (graphemes == prefix2) {
upos2 = pos;
}
}

ubrk_close(bi);

*bytes1 = upos1 ? grapheme_str_pad_utf8_offset(str, ustr, upos1) : 0;
*bytes2 = upos2 ? grapheme_str_pad_utf8_offset(str, ustr, upos2) : 0;
*count = graphemes;
efree(ustr);
return true;
}
/* }}} */

/* {{{ Writes full_copies copies of the padding string followed by its first remaining_bytes bytes. */
static char *grapheme_str_pad_fill(char *buffer, const zend_string *pad, size_t full_copies, size_t remaining_bytes)
{
if (full_copies > 0) {
size_t full_bytes = full_copies * ZSTR_LEN(pad);
if (ZSTR_LEN(pad) == 1) {
memset(buffer, ZSTR_VAL(pad)[0], full_bytes);
buffer += full_bytes;
} else {
const char *start = buffer;
const char *end = buffer + full_bytes;
buffer = (char *) zend_mempcpy(buffer, ZSTR_VAL(pad), ZSTR_LEN(pad));

/* Double the filled area on each iteration, like mb_str_pad(). */
while (buffer < end) {
size_t len = MIN((size_t) (buffer - start), (size_t) (end - buffer));
buffer = (char *) zend_mempcpy(buffer, start, len);
}
}
}
return (char *) zend_mempcpy(buffer, ZSTR_VAL(pad), remaining_bytes);
}
/* }}} */

/* {{{ Pad a string to a certain length, counted in grapheme clusters, with another string */
U_CFUNC PHP_FUNCTION(grapheme_str_pad)
{
zend_string *input, *pad = ZSTR_CHAR(' ');
zend_long pad_to_length;
zend_long pad_type_val = PHP_STR_PAD_RIGHT;

ZEND_PARSE_PARAMETERS_START(2, 4)
Z_PARAM_STR(input)
Z_PARAM_LONG(pad_to_length)
Z_PARAM_OPTIONAL
Z_PARAM_STR(pad)
Z_PARAM_LONG(pad_type_val)
ZEND_PARSE_PARAMETERS_END();

intl_error_reset(nullptr);

/* Counting stops past pad_to_length: a longer input is returned unchanged anyway. */
size_t input_length, unused;
if (!grapheme_str_pad_measure(input, pad_to_length < 0 ? 0 : (size_t) pad_to_length + 1, &input_length,
0, &unused, 0, &unused)) {
RETURN_FALSE;
}

if (pad_to_length < 0 || (size_t) pad_to_length <= input_length) {
RETURN_STR_COPY(input);
}

if (ZSTR_LEN(pad) == 0) {
zend_argument_must_not_be_empty_error(3);
RETURN_THROWS();
}

if (pad_type_val < PHP_STR_PAD_LEFT || pad_type_val > PHP_STR_PAD_BOTH) {
zend_argument_value_error(4, "must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH");
RETURN_THROWS();
}

size_t num_pad_graphemes = (size_t) pad_to_length - input_length;
size_t left_pad = 0, right_pad = 0;
switch (pad_type_val) {
case PHP_STR_PAD_RIGHT:
right_pad = num_pad_graphemes;
break;
case PHP_STR_PAD_LEFT:
left_pad = num_pad_graphemes;
break;
case PHP_STR_PAD_BOTH:
left_pad = num_pad_graphemes / 2;
right_pad = num_pad_graphemes - left_pad;
break;
}

/* Only the first MAX(left_pad, right_pad) clusters of the padding string can be needed. If it has
* that many, a prefix of it is enough on each side; otherwise it is repeated. */
size_t pad_length, left_remaining_bytes, right_remaining_bytes;
size_t full_left_copies = 0, full_right_copies = 0;
if (!grapheme_str_pad_measure(pad, MAX(left_pad, right_pad), &pad_length,
left_pad, &left_remaining_bytes, right_pad, &right_remaining_bytes)) {
RETURN_FALSE;
}
if (pad_length < MAX(left_pad, right_pad)) {
full_left_copies = left_pad / pad_length;
full_right_copies = right_pad / pad_length;
if (!grapheme_str_pad_measure(pad, pad_length, &unused,
left_pad % pad_length, &left_remaining_bytes, right_pad % pad_length, &right_remaining_bytes)) {
RETURN_FALSE;
}
}

if (UNEXPECTED(full_left_copies > (ZSTR_MAX_LEN - left_remaining_bytes) / ZSTR_LEN(pad)
|| full_right_copies > (ZSTR_MAX_LEN - right_remaining_bytes) / ZSTR_LEN(pad))) {
goto overflow;
}

{
size_t left_bytes = full_left_copies * ZSTR_LEN(pad) + left_remaining_bytes;
size_t right_bytes = full_right_copies * ZSTR_LEN(pad) + right_remaining_bytes;

if (UNEXPECTED(left_bytes > ZSTR_MAX_LEN - right_bytes
|| ZSTR_LEN(input) > ZSTR_MAX_LEN - left_bytes - right_bytes)) {
goto overflow;
}

zend_string *result = zend_string_safe_falloc(1, ZSTR_LEN(input), left_bytes + right_bytes, false, /* fallible */ true);
if (UNEXPECTED(!result)) {
RETURN_OOM();
}
char *buffer = ZSTR_VAL(result);

buffer = grapheme_str_pad_fill(buffer, pad, full_left_copies, left_remaining_bytes);
memcpy(buffer, ZSTR_VAL(input), ZSTR_LEN(input));
buffer += ZSTR_LEN(input);
buffer = grapheme_str_pad_fill(buffer, pad, full_right_copies, right_remaining_bytes);
*buffer = '\0';

RETURN_NEW_STR(result);
}

overflow:
zend_throw_error(nullptr, "String size overflow");
RETURN_THROWS();
}
/* }}} */

U_CFUNC PHP_FUNCTION(grapheme_strrev)
{
zend_string *string;
Expand Down
2 changes: 2 additions & 0 deletions ext/intl/php_intl.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,8 @@ function grapheme_levenshtein(string $string1, string $string2, int $insertion_c

function grapheme_strrev(string $string): string|false {}

function grapheme_str_pad(string $string, int $length, string $pad_string = " ", int $pad_type = STR_PAD_RIGHT): string|false {}

/** @param int $next */
function grapheme_extract(string $haystack, int $size, int $type = GRAPHEME_EXTR_COUNT, int $offset = 0, &$next = null): string|false {}

Expand Down
11 changes: 10 additions & 1 deletion ext/intl/php_intl_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 65 additions & 0 deletions ext/intl/tests/grapheme_str_pad_basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
--TEST--
grapheme_str_pad() pads to a length counted in grapheme clusters
--EXTENSIONS--
intl
--FILE--
<?php
// ASCII behaves like str_pad()
var_dump(grapheme_str_pad("abc", 6, "-") === str_pad("abc", 6, "-"));
var_dump(grapheme_str_pad("abc", 7, "xy", STR_PAD_LEFT) === str_pad("abc", 7, "xy", STR_PAD_LEFT));
var_dump(grapheme_str_pad("abc", 8, "xy", STR_PAD_BOTH) === str_pad("abc", 8, "xy", STR_PAD_BOTH));

// NFC and NFD forms of the same text get the same padding
$nfc = "M\u{00FC}ller";
$nfd = "Mu\u{0308}ller";
var_dump(grapheme_str_pad($nfc, 8, "."));
var_dump(grapheme_str_pad($nfd, 8, ".") === $nfd . "..");

// Japanese with combining dakuten, Devanagari, emoji with modifier and ZWJ sequence
var_dump(grapheme_str_pad("\u{304B}\u{3099}\u{304D}\u{3099}", 4, "*") === "\u{304B}\u{3099}\u{304D}\u{3099}**");
var_dump(grapheme_strlen(grapheme_str_pad("नमस्ते", 5, "-")));
var_dump(grapheme_str_pad("👍🏽", 3, "-", STR_PAD_LEFT) === "--👍🏽");
var_dump(grapheme_str_pad("x", 4, "👨‍👩‍👧", STR_PAD_RIGHT) === "x👨‍👩‍👧👨‍👩‍👧👨‍👩‍👧");

// The padding string is cut on a grapheme boundary
var_dump(grapheme_str_pad("a", 4, "u\u{0308}o\u{0308}") === "au\u{0308}o\u{0308}u\u{0308}");
var_dump(grapheme_str_pad("ab", 5, "\u{304B}\u{3099}x", STR_PAD_BOTH) === "\u{304B}\u{3099}ab\u{304B}\u{3099}x");

// "\r\n" is a single grapheme
var_dump(grapheme_str_pad("\r\n", 2, "-") === "\r\n-");

// Only a prefix of a long padding string is used
var_dump(grapheme_str_pad("ab", 5, "\u{304B}\u{3099}xyz", STR_PAD_BOTH) === "\u{304B}\u{3099}ab\u{304B}\u{3099}x");

// Many copies of the padding string, with a partial copy at the end
var_dump(grapheme_str_pad("x", 1001, "e\u{0301}ab", STR_PAD_LEFT) === str_repeat("e\u{0301}ab", 333) . "e\u{0301}x");
var_dump(grapheme_str_pad("x", 1002, "\u{00B7}", STR_PAD_BOTH) === str_repeat("\u{00B7}", 500) . "x" . str_repeat("\u{00B7}", 501));

// Nothing to pad
var_dump(grapheme_str_pad("abc", 3, "-"));
var_dump(grapheme_str_pad("abc", -1, "-"));
var_dump(grapheme_str_pad($nfd, 6, ".") === $nfd);
var_dump(grapheme_str_pad($nfd, 2, ".") === $nfd);
var_dump(grapheme_str_pad("", 3));
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
string(9) "Müller.."
bool(true)
bool(true)
int(5)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
string(3) "abc"
string(3) "abc"
bool(true)
bool(true)
string(3) " "
Loading
Loading