Page MenuHomec4science

utf8.php
No OneTemporary

File Metadata

Created
Mon, Jun 10, 02:40

utf8.php

<?php
/**
* Convert a string into valid UTF-8. This function is quite slow.
*
* When invalid byte subsequences are encountered, they will be replaced with
* U+FFFD, the Unicode replacement character.
*
* This function treats overlong encodings as invalid.
*
* @param string String to convert to valid UTF-8.
* @return string String with invalid UTF-8 byte subsequences replaced with
* U+FFFD.
*/
function phutil_utf8ize($string) {
if (phutil_is_utf8($string)) {
return $string;
}
// There is no function to do this in iconv, mbstring or ICU to do this, so
// do it (very very slowly) in pure PHP.
// TODO: Provide an optional fast C implementation ala fb_utf8ize() if this
// ever shows up in profiles?
$result = array();
$regex =
"/([\x01-\x7F]".
"|[\xC2-\xDF][\x80-\xBF]".
"|[\xE0][\xA0-\xBF][\x80-\xBF]".
"|[\xE1-\xEF][\x80-\xBF][\x80-\xBF]".
"|[\xF0][\x90-\xBF][\x80-\xBF][\x80-\xBF]".
"|[\xF1-\xF3][\x80-\xBF][\x80-\xBF][\x80-\xBF]".
"|[\xF4][\x80-\x8F][\x80-\xBF][\x80-\xBF])".
"|(.)/";
$replacement = "\xEF\xBF\xBD";
$offset = 0;
$matches = null;
while (preg_match($regex, $string, $matches, 0, $offset)) {
if (!isset($matches[2])) {
$match = $matches[1];
if ($match[0] == "\xED") {
// If this is a 3-byte character that may be part of one of the
// surrogate ranges, check if it's actually in those ranges. Reject
// it as invalid if it is. These sequences are used in UTF16 and
// functions like json_encode() refuse to encode them.
$codepoint = ((ord($match[0]) & 0x0F) << 12)
+ ((ord($match[1]) & 0x3F) << 6)
+ ((ord($match[2]) & 0x3F));
if ($codepoint >= 0xD800 && $codepoint <= 0xDFFF) {
$result[] = str_repeat($replacement, strlen($match));
$offset += strlen($matches[0]);
continue;
}
}
$result[] = $match;
} else {
// Unicode replacement character, U+FFFD.
$result[] = $replacement;
}
$offset += strlen($matches[0]);
}
return implode('', $result);
}
/**
* Determine if a string is valid UTF-8, with only basic multilingual plane
* characters. This is particularly important because MySQL's `utf8` column
* types silently truncate strings which contain characters outside of this
* set.
*
* @param string String to test for being valid UTF-8 with only characters in
* the basic multilingual plane.
* @return bool True if the string is valid UTF-8 with only BMP characters.
*/
function phutil_is_utf8_with_only_bmp_characters($string) {
return phutil_is_utf8_slowly($string, $only_bmp = true);
}
/**
* Determine if a string is valid UTF-8.
*
* @param string Some string which may or may not be valid UTF-8.
* @return bool True if the string is valid UTF-8.
*/
function phutil_is_utf8($string) {
if (function_exists('mb_check_encoding')) {
// If mbstring is available, this is significantly faster than using PHP.
return mb_check_encoding($string, 'UTF-8');
}
return phutil_is_utf8_slowly($string);
}
/**
* Determine if a string is valid UTF-8, slowly.
*
* This works on any system, but has very poor performance.
*
* You should call @{function:phutil_is_utf8} instead of this function, as
* that function can use more performant mechanisms if they are available on
* the system.
*
* @param string Some string which may or may not be valid UTF-8.
* @param bool True to require all characters be part of the basic
* multilingual plane (no more than 3-bytes long).
* @return bool True if the string is valid UTF-8.
*/
function phutil_is_utf8_slowly($string, $only_bmp = false) {
// First, check the common case of normal ASCII strings. We're fine if
// the string contains no bytes larger than 127.
if (preg_match('/^[\x01-\x7F]+\z/', $string)) {
return true;
}
// NOTE: In the past, we used a large regular expression in the form of
// '(x|y|z)+' to match UTF8 strings. However, PCRE can segfaults on patterns
// like this at relatively small input sizes, at least on some systems
// (observed on OSX and Windows). This is apparently because the internal
// implementation is recursive and it blows the stack.
// See <https://bugs.php.net/bug.php?id=45735> for some discussion. Since the
// input limit is extremely low (less than 50KB on my system), do this check
// very very slowly in PHP instead. See also T5316.
$len = strlen($string);
for ($ii = 0; $ii < $len; $ii++) {
$chr = ord($string[$ii]);
if ($chr >= 0x01 && $chr <= 0x7F) {
continue;
} else if ($chr >= 0xC2 && $chr <= 0xDF) {
++$ii;
if ($ii >= $len) {
return false;
}
$chr = ord($string[$ii]);
if ($chr >= 0x80 && $chr <= 0xBF) {
continue;
}
return false;
} else if ($chr > 0xE0 && $chr <= 0xEF) {
++$ii;
if ($ii >= $len) {
return false;
}
$chr = ord($string[$ii]);
if ($chr >= 0x80 && $chr <= 0xBF) {
++$ii;
if ($ii >= $len) {
return false;
}
$chr = ord($string[$ii]);
if ($chr >= 0x80 && $chr <= 0xBF) {
continue;
}
}
return false;
} else if ($chr == 0xE0) {
++$ii;
if ($ii >= $len) {
return false;
}
$chr = ord($string[$ii]);
// NOTE: This range starts at 0xA0, not 0x80. The values 0x80-0xA0 are
// "valid", but not minimal representations, and MySQL rejects them. We're
// special casing this part of the range.
if ($chr >= 0xA0 && $chr <= 0xBF) {
++$ii;
if ($ii >= $len) {
return false;
}
$chr = ord($string[$ii]);
if ($chr >= 0x80 && $chr <= 0xBF) {
continue;
}
}
return false;
} else if (!$only_bmp) {
if ($chr > 0xF0 && $chr <= 0xF4) {
++$ii;
if ($ii >= $len) {
return false;
}
$chr = ord($string[$ii]);
if ($chr >= 0x80 && $chr <= 0xBF) {
++$ii;
if ($ii >= $len) {
return false;
}
$chr = ord($string[$ii]);
if ($chr >= 0x80 && $chr <= 0xBF) {
++$ii;
if ($ii >= $len) {
return false;
}
$chr = ord($string[$ii]);
if ($chr >= 0x80 && $chr <= 0xBF) {
continue;
}
}
}
} else if ($chr == 0xF0) {
++$ii;
if ($ii >= $len) {
return false;
}
$chr = ord($string[$ii]);
// NOTE: As above, this range starts at 0x90, not 0x80. The values
// 0x80-0x90 are not minimal representations.
if ($chr >= 0x90 && $chr <= 0xBF) {
++$ii;
if ($ii >= $len) {
return false;
}
$chr = ord($string[$ii]);
if ($chr >= 0x80 && $chr <= 0xBF) {
++$ii;
if ($ii >= $len) {
return false;
}
$chr = ord($string[$ii]);
if ($chr >= 0x80 && $chr <= 0xBF) {
continue;
}
}
}
}
}
return false;
}
return true;
}
/**
* Find the character length of a UTF-8 string.
*
* @param string A valid utf-8 string.
* @return int The character length of the string.
*/
function phutil_utf8_strlen($string) {
return strlen(utf8_decode($string));
}
/**
* Find the console display length of a UTF-8 string. This may differ from the
* character length of the string if it contains double-width characters, like
* many Chinese characters.
*
* This method is based on a C implementation here, which is based on the IEEE
* standards. The source has more discussion and addresses more considerations
* than this implementation does.
*
* http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
*
* NOTE: We currently assume width 1 for East-Asian ambiguous characters.
*
* NOTE: This function is VERY slow.
*
* @param string A valid UTF-8 string.
* @return int The console display length of the string.
*/
function phutil_utf8_console_strlen($string) {
// Formatting and colors don't contribute any width in the console.
$string = preg_replace("/\x1B\[\d*m/", '', $string);
// In the common case of an ASCII string, just return the string length.
if (preg_match('/^[\x01-\x7F]*\z/', $string)) {
return strlen($string);
}
$len = 0;
// NOTE: To deal with combining characters, we're splitting the string into
// glyphs first (characters with combiners) and then counting just the width
// of the first character in each glyph.
$display_glyphs = phutil_utf8v_combined($string);
foreach ($display_glyphs as $display_glyph) {
$glyph_codepoints = phutil_utf8v_codepoints($display_glyph);
foreach ($glyph_codepoints as $c) {
if ($c == 0) {
break;
}
$len += 1 +
($c >= 0x1100 &&
($c <= 0x115F || /* Hangul Jamo init. consonants */
$c == 0x2329 || $c == 0x232A ||
($c >= 0x2E80 && $c <= 0xA4CF &&
$c != 0x303F) || /* CJK ... Yi */
($c >= 0xAC00 && $c <= 0xD7A3) || /* Hangul Syllables */
($c >= 0xF900 && $c <= 0xFAFF) || /* CJK Compatibility Ideographs */
($c >= 0xFE10 && $c <= 0xFE19) || /* Vertical forms */
($c >= 0xFE30 && $c <= 0xFE6F) || /* CJK Compatibility Forms */
($c >= 0xFF00 && $c <= 0xFF60) || /* Fullwidth Forms */
($c >= 0xFFE0 && $c <= 0xFFE6) ||
($c >= 0x20000 && $c <= 0x2FFFD) ||
($c >= 0x30000 && $c <= 0x3FFFD)));
break;
}
}
return $len;
}
/**
* Split a UTF-8 string into an array of characters. Combining characters are
* also split.
*
* @param string A valid utf-8 string.
* @param int|null Stop processing after examining this many bytes.
* @return list A list of characters in the string.
*/
function phutil_utf8v($string, $byte_limit = null) {
$res = array();
$len = strlen($string);
$ii = 0;
while ($ii < $len) {
$byte = $string[$ii];
if ($byte <= "\x7F") {
$res[] = $byte;
$ii += 1;
if ($byte_limit && ($ii >= $byte_limit)) {
break;
}
continue;
} else if ($byte < "\xC0") {
throw new Exception(
pht('Invalid UTF-8 string passed to %s.', __FUNCTION__));
} else if ($byte <= "\xDF") {
$seq_len = 2;
} else if ($byte <= "\xEF") {
$seq_len = 3;
} else if ($byte <= "\xF7") {
$seq_len = 4;
} else if ($byte <= "\xFB") {
$seq_len = 5;
} else if ($byte <= "\xFD") {
$seq_len = 6;
} else {
throw new Exception(
pht('Invalid UTF-8 string passed to %s.', __FUNCTION__));
}
if ($ii + $seq_len > $len) {
throw new Exception(
pht('Invalid UTF-8 string passed to %s.', __FUNCTION__));
}
for ($jj = 1; $jj < $seq_len; ++$jj) {
if ($string[$ii + $jj] >= "\xC0") {
throw new Exception(
pht('Invalid UTF-8 string passed to %s.', __FUNCTION__));
}
}
$res[] = substr($string, $ii, $seq_len);
$ii += $seq_len;
if ($byte_limit && ($ii >= $byte_limit)) {
break;
}
}
return $res;
}
/**
* Split a UTF-8 string into an array of codepoints (as integers).
*
* @param string A valid UTF-8 string.
* @return list A list of codepoints, as integers.
*/
function phutil_utf8v_codepoints($string) {
$str_v = phutil_utf8v($string);
foreach ($str_v as $key => $char) {
$c = ord($char[0]);
$v = 0;
if (($c & 0x80) == 0) {
$v = $c;
} else if (($c & 0xE0) == 0xC0) {
$v = (($c & 0x1F) << 6)
+ ((ord($char[1]) & 0x3F));
} else if (($c & 0xF0) == 0xE0) {
$v = (($c & 0x0F) << 12)
+ ((ord($char[1]) & 0x3F) << 6)
+ ((ord($char[2]) & 0x3F));
} else if (($c & 0xF8) == 0xF0) {
$v = (($c & 0x07) << 18)
+ ((ord($char[1]) & 0x3F) << 12)
+ ((ord($char[2]) & 0x3F) << 6)
+ ((ord($char[3]) & 0x3F));
} else if (($c & 0xFC) == 0xF8) {
$v = (($c & 0x03) << 24)
+ ((ord($char[1]) & 0x3F) << 18)
+ ((ord($char[2]) & 0x3F) << 12)
+ ((ord($char[3]) & 0x3F) << 6)
+ ((ord($char[4]) & 0x3F));
} else if (($c & 0xFE) == 0xFC) {
$v = (($c & 0x01) << 30)
+ ((ord($char[1]) & 0x3F) << 24)
+ ((ord($char[2]) & 0x3F) << 18)
+ ((ord($char[3]) & 0x3F) << 12)
+ ((ord($char[4]) & 0x3F) << 6)
+ ((ord($char[5]) & 0x3F));
}
$str_v[$key] = $v;
}
return $str_v;
}
/**
* Convert a Unicode codepoint into a UTF8-encoded string.
*
* @param int Unicode codepoint.
* @return string UTF8 encoding.
*/
function phutil_utf8_encode_codepoint($codepoint) {
if ($codepoint < 0x80) {
$r = chr($codepoint);
} else if ($codepoint < 0x800) {
$r = chr(0xC0 | (($codepoint >> 6) & 0x1F)).
chr(0x80 | (($codepoint) & 0x3F));
} else if ($codepoint < 0x10000) {
$r = chr(0xE0 | (($codepoint >> 12) & 0x0F)).
chr(0x80 | (($codepoint >> 6) & 0x3F)).
chr(0x80 | (($codepoint) & 0x3F));
} else if ($codepoint < 0x110000) {
$r = chr(0xF0 | (($codepoint >> 18) & 0x07)).
chr(0x80 | (($codepoint >> 12) & 0x3F)).
chr(0x80 | (($codepoint >> 6) & 0x3F)).
chr(0x80 | (($codepoint) & 0x3F));
} else {
throw new Exception(
pht(
'Encoding UTF8 codepoint "%s" is not supported.',
$codepoint));
}
return $r;
}
/**
* Hard-wrap a block of UTF-8 text with embedded HTML tags and entities.
*
* @param string An HTML string with tags and entities.
* @return list List of hard-wrapped lines.
*/
function phutil_utf8_hard_wrap_html($string, $width) {
$break_here = array();
// Convert the UTF-8 string into a list of UTF-8 characters.
$vector = phutil_utf8v($string);
$len = count($vector);
$char_pos = 0;
for ($ii = 0; $ii < $len; ++$ii) {
// An ampersand indicates an HTML entity; consume the whole thing (until
// ";") but treat it all as one character.
if ($vector[$ii] == '&') {
do {
++$ii;
} while ($vector[$ii] != ';');
++$char_pos;
// An "<" indicates an HTML tag, consume the whole thing but don't treat
// it as a character.
} else if ($vector[$ii] == '<') {
do {
++$ii;
} while ($vector[$ii] != '>');
} else {
++$char_pos;
}
// Keep track of where we need to break the string later.
if ($char_pos == $width) {
$break_here[$ii] = true;
$char_pos = 0;
}
}
$result = array();
$string = '';
foreach ($vector as $ii => $char) {
$string .= $char;
if (isset($break_here[$ii])) {
$result[] = $string;
$string = '';
}
}
if (strlen($string)) {
$result[] = $string;
}
return $result;
}
/**
* Hard-wrap a block of UTF-8 text with no embedded HTML tags and entities.
*
* @param string A non HTML string
* @param int Width of the hard-wrapped lines
* @return list List of hard-wrapped lines.
*/
function phutil_utf8_hard_wrap($string, $width) {
$result = array();
$lines = phutil_split_lines($string, $retain_endings = false);
foreach ($lines as $line) {
// Convert the UTF-8 string into a list of UTF-8 characters.
$vector = phutil_utf8v($line);
$len = count($vector);
$buffer = '';
for ($ii = 1; $ii <= $len; ++$ii) {
$buffer .= $vector[$ii - 1];
if (($ii % $width) === 0) {
$result[] = $buffer;
$buffer = '';
}
}
if (strlen($buffer)) {
$result[] = $buffer;
}
}
return $result;
}
/**
* Convert a string from one encoding (like ISO-8859-1) to another encoding
* (like UTF-8).
*
* This is primarily a thin wrapper around `mb_convert_encoding()` which checks
* you have the extension installed, since we try to require the extension
* only if you actually need it (i.e., you want to work with encodings other
* than UTF-8).
*
* NOTE: This function assumes that the input is in the given source encoding.
* If it is not, it may not output in the specified target encoding. If you
* need to perform a hard conversion to UTF-8, use this function in conjunction
* with @{function:phutil_utf8ize}. We can detect failures caused by invalid
* encoding names, but `mb_convert_encoding()` fails silently if the
* encoding name identifies a real encoding but the string is not actually
* encoded with that encoding.
*
* @param string String to re-encode.
* @param string Target encoding name, like "UTF-8".
* @param string Source encoding name, like "ISO-8859-1".
* @return string Input string, with converted character encoding.
*
* @phutil-external-symbol function mb_convert_encoding
*/
function phutil_utf8_convert($string, $to_encoding, $from_encoding) {
if (!$from_encoding) {
throw new InvalidArgumentException(
pht(
'Attempting to convert a string encoding, but no source encoding '.
'was provided. Explicitly provide the source encoding.'));
}
if (!$to_encoding) {
throw new InvalidArgumentException(
pht(
'Attempting to convert a string encoding, but no target encoding '.
'was provided. Explicitly provide the target encoding.'));
}
// Normalize encoding names so we can no-op the very common case of UTF8
// to UTF8 (or any other conversion where both encodings are identical).
$to_upper = strtoupper(str_replace('-', '', $to_encoding));
$from_upper = strtoupper(str_replace('-', '', $from_encoding));
if ($from_upper == $to_upper) {
return $string;
}
if (!function_exists('mb_convert_encoding')) {
throw new Exception(
pht(
"Attempting to convert a string encoding from '%s' to '%s', ".
"but the '%s' PHP extension is not available. Install %s to ".
"work with encodings other than UTF-8.",
$from_encoding,
$to_encoding,
'mbstring',
'mbstring'));
}
$result = @mb_convert_encoding($string, $to_encoding, $from_encoding);
if ($result === false) {
$message = error_get_last();
if ($message) {
$message = idx($message, 'message', pht('Unknown error.'));
}
throw new Exception(
pht(
"String conversion from encoding '%s' to encoding '%s' failed: %s",
$from_encoding,
$to_encoding,
$message));
}
return $result;
}
/**
* Convert a string to title case in a UTF8-aware way. This function doesn't
* necessarily do a great job, but the builtin implementation of `ucwords()` can
* completely destroy inputs, so it just has to be better than that. Similar to
* @{function:ucwords}.
*
* @param string UTF-8 input string.
* @return string Input, in some semblance of title case.
*/
function phutil_utf8_ucwords($str) {
// NOTE: mb_convert_case() discards uppercase letters in words when converting
// to title case. For example, it will convert "AAA" into "Aaa", which is
// undesirable.
$v = phutil_utf8v($str);
$result = '';
$last = null;
$ord_a = ord('a');
$ord_z = ord('z');
foreach ($v as $c) {
$convert = false;
if ($last === null || $last === ' ') {
$o = ord($c[0]);
if ($o >= $ord_a && $o <= $ord_z) {
$convert = true;
}
}
if ($convert) {
$result .= phutil_utf8_strtoupper($c);
} else {
$result .= $c;
}
$last = $c;
}
return $result;
}
/**
* Convert a string to lower case in a UTF8-aware way. Similar to
* @{function:strtolower}.
*
* @param string UTF-8 input string.
* @return string Input, in some semblance of lower case.
*
* @phutil-external-symbol function mb_convert_case
*/
function phutil_utf8_strtolower($str) {
if (function_exists('mb_convert_case')) {
return mb_convert_case($str, MB_CASE_LOWER, 'UTF-8');
}
static $map;
if ($map === null) {
$map = array_combine(
range('A', 'Z'),
range('a', 'z'));
}
return phutil_utf8_strtr($str, $map);
}
/**
* Convert a string to upper case in a UTF8-aware way. Similar to
* @{function:strtoupper}.
*
* @param string UTF-8 input string.
* @return string Input, in some semblance of upper case.
*
* @phutil-external-symbol function mb_convert_case
*/
function phutil_utf8_strtoupper($str) {
if (function_exists('mb_convert_case')) {
return mb_convert_case($str, MB_CASE_UPPER, 'UTF-8');
}
static $map;
if ($map === null) {
$map = array_combine(
range('a', 'z'),
range('A', 'Z'));
}
return phutil_utf8_strtr($str, $map);
}
/**
* Replace characters in a string in a UTF-aware way. Similar to
* @{function:strtr}.
*
* @param string UTF-8 input string.
* @param map<string, string> Map of characters to replace.
* @return string Input with translated characters.
*/
function phutil_utf8_strtr($str, array $map) {
$v = phutil_utf8v($str);
$result = '';
foreach ($v as $c) {
if (isset($map[$c])) {
$result .= $map[$c];
} else {
$result .= $c;
}
}
return $result;
}
/**
* Determine if a given unicode character is a combining character or not.
*
* @param string A single unicode character.
* @return boolean True or false.
*/
function phutil_utf8_is_combining_character($character) {
$components = phutil_utf8v_codepoints($character);
// Combining Diacritical Marks (0300 - 036F).
// Combining Diacritical Marks Supplement (1DC0 - 1DFF).
// Combining Diacritical Marks for Symbols (20D0 - 20FF).
// Combining Half Marks (FE20 - FE2F).
foreach ($components as $codepoint) {
if ($codepoint >= 0x0300 && $codepoint <= 0x036F ||
$codepoint >= 0x1DC0 && $codepoint <= 0x1DFF ||
$codepoint >= 0x20D0 && $codepoint <= 0x20FF ||
$codepoint >= 0xFE20 && $codepoint <= 0xFE2F) {
return true;
}
}
return false;
}
/**
* Split a UTF-8 string into an array of characters. Combining characters
* are not split.
*
* @param string A valid utf-8 string.
* @return list A list of characters in the string.
*/
function phutil_utf8v_combined($string) {
$components = phutil_utf8v($string);
return phutil_utf8v_combine_characters($components);
}
/**
* Merge combining characters in a UTF-8 string.
*
* This is a low-level method which can allow other operations to do less work.
* If you have a string, call @{method:phutil_utf8v_combined} instead.
*
* @param list List of UTF-8 characters.
* @return list List of UTF-8 strings with combining characters merged.
*/
function phutil_utf8v_combine_characters(array $characters) {
if (!$characters) {
return array();
}
// If the first character in the string is a combining character,
// start with a space.
if (phutil_utf8_is_combining_character($characters[0])) {
$buf = ' ';
} else {
$buf = null;
}
$parts = array();
foreach ($characters as $character) {
if (!isset($character[1])) {
// This an optimization: there are no one-byte combining characters,
// so we can just pass these through unmodified.
$is_combining = false;
} else {
$is_combining = phutil_utf8_is_combining_character($character);
}
if ($is_combining) {
$buf .= $character;
} else {
if ($buf !== null) {
$parts[] = $buf;
}
$buf = $character;
}
}
$parts[] = $buf;
return $parts;
}

Event Timeline