2012-12-11 02:53:43 +00:00
|
|
|
|
<?php
|
|
|
|
|
|
2018-03-22 10:06:15 +00:00
|
|
|
|
use UtfNormal\Validator;
|
|
|
|
|
|
2012-12-11 02:53:43 +00:00
|
|
|
|
class Scribunto_LuaUstringLibrary extends Scribunto_LuaLibraryBase {
|
|
|
|
|
/**
|
|
|
|
|
* Limit on pattern lengths, in bytes not characters
|
2020-11-04 09:55:45 +00:00
|
|
|
|
* @var int
|
2012-12-11 02:53:43 +00:00
|
|
|
|
*/
|
|
|
|
|
private $patternLengthLimit = 10000;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Limit on string lengths, in bytes not characters
|
|
|
|
|
* If null, $wgMaxArticleSize * 1024 will be used
|
2020-11-04 09:55:45 +00:00
|
|
|
|
* @var int|null
|
2012-12-11 02:53:43 +00:00
|
|
|
|
*/
|
|
|
|
|
private $stringLengthLimit = null;
|
|
|
|
|
|
2015-09-23 17:31:54 +00:00
|
|
|
|
/**
|
|
|
|
|
* PHP until 5.6.9 are buggy when the regex in preg_replace an
|
|
|
|
|
* preg_match_all matches the empty string.
|
2020-11-04 09:55:45 +00:00
|
|
|
|
* @var bool
|
2015-09-23 17:31:54 +00:00
|
|
|
|
*/
|
|
|
|
|
private $phpBug53823 = false;
|
|
|
|
|
|
2014-07-11 19:39:32 +00:00
|
|
|
|
/**
|
|
|
|
|
* A cache of patterns and the regexes they generate.
|
2018-04-09 04:39:06 +00:00
|
|
|
|
* @var MapCacheLRU
|
2014-07-11 19:39:32 +00:00
|
|
|
|
*/
|
|
|
|
|
private $patternRegexCache = null;
|
|
|
|
|
|
2020-01-14 18:50:34 +00:00
|
|
|
|
/** @inheritDoc */
|
2018-11-09 19:31:08 +00:00
|
|
|
|
public function __construct( $engine ) {
|
2012-12-11 02:53:43 +00:00
|
|
|
|
if ( $this->stringLengthLimit === null ) {
|
|
|
|
|
global $wgMaxArticleSize;
|
|
|
|
|
$this->stringLengthLimit = $wgMaxArticleSize * 1024;
|
|
|
|
|
}
|
|
|
|
|
|
2015-09-23 17:31:54 +00:00
|
|
|
|
$this->phpBug53823 = preg_replace( '//us', 'x', "\xc3\xa1" ) === "x\xc3x\xa1x";
|
2014-07-11 19:39:32 +00:00
|
|
|
|
$this->patternRegexCache = new MapCacheLRU( 100 );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
|
|
|
|
|
parent::__construct( $engine );
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
public function register() {
|
2012-12-11 02:53:43 +00:00
|
|
|
|
$perf = $this->getEngine()->getPerformanceCharacteristics();
|
|
|
|
|
|
|
|
|
|
if ( $perf['phpCallsRequireSerialization'] ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
$lib = [
|
2012-12-11 02:53:43 +00:00
|
|
|
|
// Pattern matching is still much faster in PHP, even with the
|
|
|
|
|
// overhead of serialization
|
2017-06-15 17:19:00 +00:00
|
|
|
|
'find' => [ $this, 'ustringFind' ],
|
|
|
|
|
'match' => [ $this, 'ustringMatch' ],
|
|
|
|
|
'gmatch_init' => [ $this, 'ustringGmatchInit' ],
|
|
|
|
|
'gmatch_callback' => [ $this, 'ustringGmatchCallback' ],
|
|
|
|
|
'gsub' => [ $this, 'ustringGsub' ],
|
|
|
|
|
];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
} else {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
$lib = [
|
|
|
|
|
'isutf8' => [ $this, 'ustringIsUtf8' ],
|
|
|
|
|
'byteoffset' => [ $this, 'ustringByteoffset' ],
|
|
|
|
|
'codepoint' => [ $this, 'ustringCodepoint' ],
|
|
|
|
|
'gcodepoint_init' => [ $this, 'ustringGcodepointInit' ],
|
|
|
|
|
'toNFC' => [ $this, 'ustringToNFC' ],
|
|
|
|
|
'toNFD' => [ $this, 'ustringToNFD' ],
|
|
|
|
|
'toNFKC' => [ $this, 'ustringToNFKC' ],
|
|
|
|
|
'toNFKD' => [ $this, 'ustringToNFKD' ],
|
|
|
|
|
'char' => [ $this, 'ustringChar' ],
|
|
|
|
|
'len' => [ $this, 'ustringLen' ],
|
|
|
|
|
'sub' => [ $this, 'ustringSub' ],
|
|
|
|
|
'upper' => [ $this, 'ustringUpper' ],
|
|
|
|
|
'lower' => [ $this, 'ustringLower' ],
|
|
|
|
|
'find' => [ $this, 'ustringFind' ],
|
|
|
|
|
'match' => [ $this, 'ustringMatch' ],
|
|
|
|
|
'gmatch_init' => [ $this, 'ustringGmatchInit' ],
|
|
|
|
|
'gmatch_callback' => [ $this, 'ustringGmatchCallback' ],
|
|
|
|
|
'gsub' => [ $this, 'ustringGsub' ],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
return $this->getEngine()->registerInterface( 'mw.ustring.lua', $lib, [
|
2012-12-11 02:53:43 +00:00
|
|
|
|
'stringLengthLimit' => $this->stringLengthLimit,
|
|
|
|
|
'patternLengthLimit' => $this->patternLengthLimit,
|
2017-06-15 17:19:00 +00:00
|
|
|
|
] );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-14 18:50:34 +00:00
|
|
|
|
/**
|
|
|
|
|
* Check a string first parameter
|
|
|
|
|
* @param string $name Function name, for errors
|
|
|
|
|
* @param mixed $s Value to check
|
|
|
|
|
* @param bool $checkEncoding Whether to validate UTF-8 encoding.
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
private function checkString( $name, $s, $checkEncoding = true ) {
|
2014-06-27 16:31:04 +00:00
|
|
|
|
if ( $this->getLuaType( $s ) == 'number' ) {
|
|
|
|
|
$s = (string)$s;
|
2014-07-11 19:39:32 +00:00
|
|
|
|
} else {
|
|
|
|
|
$this->checkType( $name, 1, $s, 'string' );
|
2017-08-21 21:16:32 +00:00
|
|
|
|
if ( $checkEncoding && !mb_check_encoding( $s, 'UTF-8' ) ) {
|
2014-07-11 19:39:32 +00:00
|
|
|
|
throw new Scribunto_LuaError( "bad argument #1 to '$name' (string is not UTF-8)" );
|
|
|
|
|
}
|
|
|
|
|
if ( strlen( $s ) > $this->stringLengthLimit ) {
|
|
|
|
|
throw new Scribunto_LuaError(
|
|
|
|
|
"bad argument #1 to '$name' (string is longer than $this->stringLengthLimit bytes)"
|
|
|
|
|
);
|
|
|
|
|
}
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for isUtf8
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @return bool[]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringIsUtf8( $s ) {
|
|
|
|
|
$this->checkString( 'isutf8', $s, false );
|
2017-08-21 21:16:32 +00:00
|
|
|
|
return [ mb_check_encoding( $s, 'UTF-8' ) ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for byteoffset
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @param int $l
|
|
|
|
|
* @param int $i
|
|
|
|
|
* @return int[]|null[]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringByteoffset( $s, $l = 1, $i = 1 ) {
|
|
|
|
|
$this->checkString( 'byteoffset', $s );
|
|
|
|
|
$this->checkTypeOptional( 'byteoffset', 2, $l, 'number', 1 );
|
|
|
|
|
$this->checkTypeOptional( 'byteoffset', 3, $i, 'number', 1 );
|
|
|
|
|
|
|
|
|
|
$bytelen = strlen( $s );
|
|
|
|
|
if ( $i < 0 ) {
|
2013-06-25 14:06:01 +00:00
|
|
|
|
$i = $bytelen + $i + 1;
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
if ( $i < 1 || $i > $bytelen ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ null ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
$i--;
|
|
|
|
|
$j = $i;
|
|
|
|
|
while ( ( ord( $s[$i] ) & 0xc0 ) === 0x80 ) {
|
|
|
|
|
$i--;
|
|
|
|
|
}
|
|
|
|
|
if ( $l > 0 && $j === $i ) {
|
|
|
|
|
$l--;
|
|
|
|
|
}
|
|
|
|
|
$char = mb_strlen( substr( $s, 0, $i ), 'UTF-8' ) + $l;
|
|
|
|
|
if ( $char < 0 || $char >= mb_strlen( $s, 'UTF-8' ) ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ null ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
} else {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ strlen( mb_substr( $s, 0, $char, 'UTF-8' ) ) + 1 ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for codepoint
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @param int $i
|
|
|
|
|
* @param int|null $j
|
|
|
|
|
* @return int[]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringCodepoint( $s, $i = 1, $j = null ) {
|
|
|
|
|
$this->checkString( 'codepoint', $s );
|
|
|
|
|
$this->checkTypeOptional( 'codepoint', 2, $i, 'number', 1 );
|
|
|
|
|
$this->checkTypeOptional( 'codepoint', 3, $j, 'number', $i );
|
|
|
|
|
|
|
|
|
|
$l = mb_strlen( $s, 'UTF-8' );
|
|
|
|
|
if ( $i < 0 ) {
|
|
|
|
|
$i = $l + $i + 1;
|
|
|
|
|
}
|
|
|
|
|
if ( $j < 0 ) {
|
|
|
|
|
$j = $l + $j + 1;
|
|
|
|
|
}
|
2013-07-03 15:41:25 +00:00
|
|
|
|
if ( $j < $i ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [];
|
2013-07-03 15:41:25 +00:00
|
|
|
|
}
|
|
|
|
|
$i = max( 1, min( $i, $l + 1 ) );
|
|
|
|
|
$j = max( 1, min( $j, $l + 1 ) );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
$s = mb_substr( $s, $i - 1, $j - $i + 1, 'UTF-8' );
|
|
|
|
|
return unpack( 'N*', mb_convert_encoding( $s, 'UTF-32BE', 'UTF-8' ) );
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for gcodepointInit
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @param int $i
|
|
|
|
|
* @param int|null $j
|
|
|
|
|
* @return int[][]
|
|
|
|
|
*/
|
2015-11-16 17:39:03 +00:00
|
|
|
|
public function ustringGcodepointInit( $s, $i = 1, $j = null ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ $this->ustringCodepoint( $s, $i, $j ) ];
|
2015-11-16 17:39:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for toNFC
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @return string[]|null[]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringToNFC( $s ) {
|
|
|
|
|
$this->checkString( 'toNFC', $s, false );
|
2017-08-21 21:16:32 +00:00
|
|
|
|
if ( !mb_check_encoding( $s, 'UTF-8' ) ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ null ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
2018-03-22 10:06:15 +00:00
|
|
|
|
return [ Validator::toNFC( $s ) ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for toNFD
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @return string[]|null[]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringToNFD( $s ) {
|
|
|
|
|
$this->checkString( 'toNFD', $s, false );
|
2017-08-21 21:16:32 +00:00
|
|
|
|
if ( !mb_check_encoding( $s, 'UTF-8' ) ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ null ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
2018-03-22 10:06:15 +00:00
|
|
|
|
return [ Validator::toNFD( $s ) ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for toNFKC
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @return string[]|null[]
|
|
|
|
|
*/
|
2016-04-01 10:54:42 +00:00
|
|
|
|
public function ustringToNFKC( $s ) {
|
|
|
|
|
$this->checkString( 'toNFKC', $s, false );
|
2017-08-21 21:16:32 +00:00
|
|
|
|
if ( !mb_check_encoding( $s, 'UTF-8' ) ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ null ];
|
2016-04-01 10:54:42 +00:00
|
|
|
|
}
|
2018-03-22 10:06:15 +00:00
|
|
|
|
return [ Validator::toNFKC( $s ) ];
|
2016-04-01 10:54:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for toNFKD
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @return string[]|null[]
|
|
|
|
|
*/
|
2016-04-01 10:54:42 +00:00
|
|
|
|
public function ustringToNFKD( $s ) {
|
|
|
|
|
$this->checkString( 'toNFKD', $s, false );
|
2017-08-21 21:16:32 +00:00
|
|
|
|
if ( !mb_check_encoding( $s, 'UTF-8' ) ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ null ];
|
2016-04-01 10:54:42 +00:00
|
|
|
|
}
|
2018-03-22 10:06:15 +00:00
|
|
|
|
return [ Validator::toNFKD( $s ) ];
|
2016-04-01 10:54:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-04-09 04:39:06 +00:00
|
|
|
|
/**
|
2018-11-09 19:31:08 +00:00
|
|
|
|
* Handler for char
|
|
|
|
|
* @internal
|
|
|
|
|
* @return string[]
|
2018-04-09 04:39:06 +00:00
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringChar() {
|
|
|
|
|
$args = func_get_args();
|
|
|
|
|
if ( count( $args ) > $this->stringLengthLimit ) {
|
2018-04-09 04:39:06 +00:00
|
|
|
|
throw new Scribunto_LuaError( "too many arguments to 'char'" );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
2017-08-11 04:28:16 +00:00
|
|
|
|
foreach ( $args as $k => &$v ) {
|
2012-12-11 02:53:43 +00:00
|
|
|
|
if ( !is_numeric( $v ) ) {
|
2017-08-11 04:28:16 +00:00
|
|
|
|
$this->checkType( 'char', $k + 1, $v, 'number' );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
$v = (int)floor( $v );
|
|
|
|
|
if ( $v < 0 || $v > 0x10ffff ) {
|
|
|
|
|
$k++;
|
|
|
|
|
throw new Scribunto_LuaError( "bad argument #$k to 'char' (value out of range)" );
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-06-08 07:56:03 +00:00
|
|
|
|
$s = pack( 'N*', ...$args );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
$s = mb_convert_encoding( $s, 'UTF-8', 'UTF-32BE' );
|
|
|
|
|
if ( strlen( $s ) > $this->stringLengthLimit ) {
|
2018-04-09 04:39:06 +00:00
|
|
|
|
throw new Scribunto_LuaError( "result to long for 'char'" );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ $s ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for len
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @return int[]|null[]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringLen( $s ) {
|
|
|
|
|
$this->checkString( 'len', $s, false );
|
2017-08-21 21:16:32 +00:00
|
|
|
|
if ( !mb_check_encoding( $s, 'UTF-8' ) ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ null ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ mb_strlen( $s, 'UTF-8' ) ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for sub
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @param int $i
|
|
|
|
|
* @param int $j
|
|
|
|
|
* @return string[]
|
|
|
|
|
*/
|
2018-09-03 18:08:53 +00:00
|
|
|
|
public function ustringSub( $s, $i = 1, $j = -1 ) {
|
2012-12-11 02:53:43 +00:00
|
|
|
|
$this->checkString( 'sub', $s );
|
|
|
|
|
$this->checkTypeOptional( 'sub', 2, $i, 'number', 1 );
|
|
|
|
|
$this->checkTypeOptional( 'sub', 3, $j, 'number', -1 );
|
|
|
|
|
|
|
|
|
|
$len = mb_strlen( $s, 'UTF-8' );
|
|
|
|
|
if ( $i < 0 ) {
|
|
|
|
|
$i = $len + $i + 1;
|
|
|
|
|
}
|
|
|
|
|
if ( $j < 0 ) {
|
|
|
|
|
$j = $len + $j + 1;
|
|
|
|
|
}
|
2013-07-03 15:41:25 +00:00
|
|
|
|
if ( $j < $i ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ '' ];
|
2013-07-03 15:41:25 +00:00
|
|
|
|
}
|
2012-12-11 02:53:43 +00:00
|
|
|
|
$i = max( 1, min( $i, $len + 1 ) );
|
|
|
|
|
$j = max( 1, min( $j, $len + 1 ) );
|
|
|
|
|
$s = mb_substr( $s, $i - 1, $j - $i + 1, 'UTF-8' );
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ $s ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for upper
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @return string[]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringUpper( $s ) {
|
|
|
|
|
$this->checkString( 'upper', $s );
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ mb_strtoupper( $s, 'UTF-8' ) ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for lower
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @return string[]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringLower( $s ) {
|
|
|
|
|
$this->checkString( 'lower', $s );
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ mb_strtolower( $s, 'UTF-8' ) ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-14 18:50:34 +00:00
|
|
|
|
/**
|
|
|
|
|
* Check a pattern as the second argument
|
|
|
|
|
* @param string $name Lua function name, for errors
|
|
|
|
|
* @param mixed $pattern Lua pattern
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
private function checkPattern( $name, $pattern ) {
|
2014-06-27 16:31:04 +00:00
|
|
|
|
if ( $this->getLuaType( $pattern ) == 'number' ) {
|
|
|
|
|
$pattern = (string)$pattern;
|
|
|
|
|
}
|
2012-12-11 02:53:43 +00:00
|
|
|
|
$this->checkType( $name, 2, $pattern, 'string' );
|
2017-08-21 21:16:32 +00:00
|
|
|
|
if ( !mb_check_encoding( $pattern, 'UTF-8' ) ) {
|
2012-12-11 02:53:43 +00:00
|
|
|
|
throw new Scribunto_LuaError( "bad argument #2 to '$name' (string is not UTF-8)" );
|
|
|
|
|
}
|
|
|
|
|
if ( strlen( $pattern ) > $this->patternLengthLimit ) {
|
|
|
|
|
throw new Scribunto_LuaError(
|
|
|
|
|
"bad argument #2 to '$name' (pattern is longer than $this->patternLengthLimit bytes)"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-14 18:50:34 +00:00
|
|
|
|
/**
|
|
|
|
|
* Convert a Lua pattern into a PCRE regex
|
|
|
|
|
* @param string $pattern Lua pattern to convert
|
|
|
|
|
* @param string|false $anchor Regex fragment (`^` or `\G`) to use
|
|
|
|
|
* when anchoring the start of the regex, or false to disable start-anchoring.
|
|
|
|
|
* @param string $name Lua function name, for errors
|
|
|
|
|
* @return array [ string $re, array $capt, bool $anypos ]
|
|
|
|
|
* - $re: The regular expression
|
|
|
|
|
* - $capt: Definition of capturing groups, see addCapturesFromMatch()
|
|
|
|
|
* - $anypos: Whether any positional captures were encountered in the pattern.
|
|
|
|
|
*/
|
2014-07-11 19:39:32 +00:00
|
|
|
|
private function patternToRegex( $pattern, $anchor, $name ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
$cacheKey = serialize( [ $pattern, $anchor ] );
|
2014-07-11 19:39:32 +00:00
|
|
|
|
if ( !$this->patternRegexCache->has( $cacheKey ) ) {
|
|
|
|
|
$this->checkPattern( $name, $pattern );
|
|
|
|
|
$pat = preg_split( '//us', $pattern, null, PREG_SPLIT_NO_EMPTY );
|
|
|
|
|
|
|
|
|
|
static $charsets = null, $brcharsets = null;
|
|
|
|
|
if ( $charsets === null ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
$charsets = [
|
2014-07-11 19:39:32 +00:00
|
|
|
|
// If you change these, also change lualib/ustring/make-tables.php
|
|
|
|
|
// (and run it to regenerate charsets.lua)
|
|
|
|
|
'a' => '\p{L}',
|
|
|
|
|
'c' => '\p{Cc}',
|
|
|
|
|
'd' => '\p{Nd}',
|
|
|
|
|
'l' => '\p{Ll}',
|
|
|
|
|
'p' => '\p{P}',
|
|
|
|
|
's' => '\p{Xps}',
|
|
|
|
|
'u' => '\p{Lu}',
|
|
|
|
|
'w' => '[\p{L}\p{Nd}]',
|
|
|
|
|
'x' => '[0-9A-Fa-f0-9A-Fa-f]',
|
|
|
|
|
'z' => '\0',
|
|
|
|
|
|
|
|
|
|
// These *must* be the inverse of the above
|
|
|
|
|
'A' => '\P{L}',
|
|
|
|
|
'C' => '\P{Cc}',
|
|
|
|
|
'D' => '\P{Nd}',
|
|
|
|
|
'L' => '\P{Ll}',
|
|
|
|
|
'P' => '\P{P}',
|
|
|
|
|
'S' => '\P{Xps}',
|
|
|
|
|
'U' => '\P{Lu}',
|
|
|
|
|
'W' => '[^\p{L}\p{Nd}]',
|
|
|
|
|
'X' => '[^0-9A-Fa-f0-9A-Fa-f]',
|
|
|
|
|
'Z' => '[^\0]',
|
2017-06-15 17:19:00 +00:00
|
|
|
|
];
|
|
|
|
|
$brcharsets = [
|
2014-07-11 19:39:32 +00:00
|
|
|
|
'w' => '\p{L}\p{Nd}',
|
|
|
|
|
'x' => '0-9A-Fa-f0-9A-Fa-f',
|
|
|
|
|
|
|
|
|
|
// Negated sets that are not expressable as a simple \P{} are
|
|
|
|
|
// unfortunately complicated.
|
|
|
|
|
|
|
|
|
|
// Xan is L plus N, so ^Xan plus Nl plus No is anything that's not L or Nd
|
|
|
|
|
'W' => '\P{Xan}\p{Nl}\p{No}',
|
|
|
|
|
|
|
|
|
|
// Manually constructed. Fun.
|
|
|
|
|
'X' => '\x00-\x2f\x3a-\x40\x47-\x60\x67-\x{ff0f}'
|
|
|
|
|
. '\x{ff1a}-\x{ff20}\x{ff27}-\x{ff40}\x{ff47}-\x{10ffff}',
|
|
|
|
|
|
|
|
|
|
// Ha!
|
|
|
|
|
'Z' => '\x01-\x{10ffff}',
|
2017-06-15 17:19:00 +00:00
|
|
|
|
] + $charsets;
|
2014-07-11 19:39:32 +00:00
|
|
|
|
}
|
2012-12-11 02:53:43 +00:00
|
|
|
|
|
2014-07-11 19:39:32 +00:00
|
|
|
|
$re = '/';
|
|
|
|
|
$len = count( $pat );
|
2017-06-15 17:19:00 +00:00
|
|
|
|
$capt = [];
|
2014-07-11 19:39:32 +00:00
|
|
|
|
$anypos = false;
|
2017-06-15 17:19:00 +00:00
|
|
|
|
$captparen = [];
|
|
|
|
|
$opencapt = [];
|
2014-07-11 19:39:32 +00:00
|
|
|
|
$bct = 0;
|
2015-09-23 17:31:54 +00:00
|
|
|
|
|
2014-07-11 19:39:32 +00:00
|
|
|
|
for ( $i = 0; $i < $len; $i++ ) {
|
|
|
|
|
$ii = $i + 1;
|
|
|
|
|
$q = false;
|
|
|
|
|
switch ( $pat[$i] ) {
|
|
|
|
|
case '^':
|
|
|
|
|
$q = $i;
|
|
|
|
|
$re .= ( $anchor === false || $q ) ? '\\^' : $anchor;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case '$':
|
|
|
|
|
$q = ( $i < $len - 1 );
|
|
|
|
|
$re .= $q ? '\\$' : '$';
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case '(':
|
|
|
|
|
if ( $i + 1 >= $len ) {
|
|
|
|
|
throw new Scribunto_LuaError( "Unmatched open-paren at pattern character $ii" );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
2014-07-11 19:39:32 +00:00
|
|
|
|
$n = count( $capt ) + 1;
|
|
|
|
|
$capt[$n] = ( $pat[$i + 1] === ')' );
|
|
|
|
|
if ( $capt[$n] ) {
|
|
|
|
|
$anypos = true;
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
2014-07-11 19:39:32 +00:00
|
|
|
|
$re .= "(?<m$n>";
|
|
|
|
|
$opencapt[] = $n;
|
|
|
|
|
$captparen[$n] = $ii;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case ')':
|
|
|
|
|
if ( count( $opencapt ) <= 0 ) {
|
|
|
|
|
throw new Scribunto_LuaError( "Unmatched close-paren at pattern character $ii" );
|
2013-04-19 19:26:45 +00:00
|
|
|
|
}
|
2014-07-11 19:39:32 +00:00
|
|
|
|
array_pop( $opencapt );
|
|
|
|
|
$re .= $pat[$i];
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case '%':
|
|
|
|
|
$i++;
|
|
|
|
|
if ( $i >= $len ) {
|
|
|
|
|
throw new Scribunto_LuaError( "malformed pattern (ends with '%')" );
|
2013-04-19 19:26:45 +00:00
|
|
|
|
}
|
2014-07-11 19:39:32 +00:00
|
|
|
|
if ( isset( $charsets[$pat[$i]] ) ) {
|
|
|
|
|
$re .= $charsets[$pat[$i]];
|
|
|
|
|
$q = true;
|
|
|
|
|
} elseif ( $pat[$i] === 'b' ) {
|
|
|
|
|
if ( $i + 2 >= $len ) {
|
|
|
|
|
throw new Scribunto_LuaError( "malformed pattern (missing arguments to \'%b\')" );
|
|
|
|
|
}
|
|
|
|
|
$d1 = preg_quote( $pat[++$i], '/' );
|
|
|
|
|
$d2 = preg_quote( $pat[++$i], '/' );
|
|
|
|
|
if ( $d1 === $d2 ) {
|
|
|
|
|
$re .= "{$d1}[^$d1]*$d1";
|
|
|
|
|
} else {
|
|
|
|
|
$bct++;
|
|
|
|
|
$re .= "(?<b$bct>$d1(?:(?>[^$d1$d2]+)|(?P>b$bct))*$d2)";
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $pat[$i] === 'f' ) {
|
|
|
|
|
if ( $i + 1 >= $len || $pat[++$i] !== '[' ) {
|
|
|
|
|
throw new Scribunto_LuaError( "missing '[' after %f in pattern at pattern character $ii" );
|
|
|
|
|
}
|
|
|
|
|
list( $i, $re2 ) = $this->bracketedCharSetToRegex( $pat, $i, $len, $brcharsets );
|
|
|
|
|
// Because %f considers the beginning and end of the string
|
|
|
|
|
// to be \0, determine if $re2 matches that and take it
|
|
|
|
|
// into account with "^" and "$".
|
2019-03-21 04:19:37 +00:00
|
|
|
|
// @phan-suppress-next-line PhanParamSuspiciousOrder
|
2014-07-11 19:39:32 +00:00
|
|
|
|
if ( preg_match( "/$re2/us", "\0" ) ) {
|
|
|
|
|
$re .= "(?<!^)(?<!$re2)(?=$re2|$)";
|
|
|
|
|
} else {
|
|
|
|
|
$re .= "(?<!$re2)(?=$re2)";
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $pat[$i] >= '0' && $pat[$i] <= '9' ) {
|
|
|
|
|
$n = ord( $pat[$i] ) - 0x30;
|
|
|
|
|
if ( $n === 0 || $n > count( $capt ) || in_array( $n, $opencapt ) ) {
|
|
|
|
|
throw new Scribunto_LuaError( "invalid capture index %$n at pattern character $ii" );
|
|
|
|
|
}
|
|
|
|
|
$re .= "\\g{m$n}";
|
|
|
|
|
} else {
|
|
|
|
|
$re .= preg_quote( $pat[$i], '/' );
|
|
|
|
|
$q = true;
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
2014-07-11 19:39:32 +00:00
|
|
|
|
break;
|
2012-12-11 02:53:43 +00:00
|
|
|
|
|
2014-07-11 19:39:32 +00:00
|
|
|
|
case '[':
|
|
|
|
|
list( $i, $re2 ) = $this->bracketedCharSetToRegex( $pat, $i, $len, $brcharsets );
|
|
|
|
|
$re .= $re2;
|
|
|
|
|
$q = true;
|
|
|
|
|
break;
|
2012-12-11 02:53:43 +00:00
|
|
|
|
|
2014-07-11 19:39:32 +00:00
|
|
|
|
case ']':
|
|
|
|
|
throw new Scribunto_LuaError( "Unmatched close-bracket at pattern character $ii" );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
|
2014-07-11 19:39:32 +00:00
|
|
|
|
case '.':
|
|
|
|
|
$re .= $pat[$i];
|
|
|
|
|
$q = true;
|
2012-12-11 02:53:43 +00:00
|
|
|
|
break;
|
2014-07-11 19:39:32 +00:00
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
$re .= preg_quote( $pat[$i], '/' );
|
|
|
|
|
$q = true;
|
2012-12-11 02:53:43 +00:00
|
|
|
|
break;
|
|
|
|
|
}
|
2014-07-11 19:39:32 +00:00
|
|
|
|
if ( $q && $i + 1 < $len ) {
|
|
|
|
|
switch ( $pat[$i + 1] ) {
|
|
|
|
|
case '*':
|
|
|
|
|
case '+':
|
|
|
|
|
case '?':
|
|
|
|
|
$re .= $pat[++$i];
|
|
|
|
|
break;
|
|
|
|
|
case '-':
|
|
|
|
|
$re .= '*?';
|
|
|
|
|
$i++;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
2014-07-11 19:39:32 +00:00
|
|
|
|
if ( count( $opencapt ) ) {
|
|
|
|
|
$ii = $captparen[$opencapt[0]];
|
|
|
|
|
throw new Scribunto_LuaError( "Unclosed capture beginning at pattern character $ii" );
|
|
|
|
|
}
|
|
|
|
|
$re .= '/us';
|
|
|
|
|
|
2017-06-15 17:19:00 +00:00
|
|
|
|
$this->patternRegexCache->set( $cacheKey, [ $re, $capt, $anypos ] );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
2014-07-11 19:39:32 +00:00
|
|
|
|
return $this->patternRegexCache->get( $cacheKey );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-14 18:50:34 +00:00
|
|
|
|
/**
|
|
|
|
|
* Convert a Lua pattern bracketed character set to a PCRE regex fragment
|
|
|
|
|
* @param string[] $pat Pattern being processed, split into individual characters.
|
|
|
|
|
* @param int $i Offset of the start of the bracketed character set in $pat.
|
|
|
|
|
* @param int $len Length of $pat.
|
|
|
|
|
* @param array $brcharsets Mapping from Lua pattern percent escapes to
|
|
|
|
|
* regex-style character ranges.
|
|
|
|
|
* @return array [ int $new_i, string $re_fragment ]
|
|
|
|
|
*/
|
2015-06-26 16:37:34 +00:00
|
|
|
|
private function bracketedCharSetToRegex( $pat, $i, $len, $brcharsets ) {
|
2013-04-19 19:26:45 +00:00
|
|
|
|
$ii = $i + 1;
|
|
|
|
|
$re = '[';
|
|
|
|
|
$i++;
|
|
|
|
|
if ( $i < $len && $pat[$i] === '^' ) {
|
|
|
|
|
$re .= '^';
|
|
|
|
|
$i++;
|
|
|
|
|
}
|
2015-04-17 15:02:48 +00:00
|
|
|
|
for ( $j = $i; $i < $len && ( $j == $i || $pat[$i] !== ']' ); $i++ ) {
|
2013-04-19 19:26:45 +00:00
|
|
|
|
if ( $pat[$i] === '%' ) {
|
|
|
|
|
$i++;
|
|
|
|
|
if ( $i >= $len ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if ( isset( $brcharsets[$pat[$i]] ) ) {
|
|
|
|
|
$re .= $brcharsets[$pat[$i]];
|
|
|
|
|
} else {
|
|
|
|
|
$re .= preg_quote( $pat[$i], '/' );
|
|
|
|
|
}
|
2016-05-17 14:52:05 +00:00
|
|
|
|
} elseif ( $i + 2 < $len &&
|
|
|
|
|
$pat[$i + 1] === '-' && $pat[$i + 2] !== ']' && $pat[$i + 2] !== '%'
|
|
|
|
|
) {
|
2017-08-11 04:28:16 +00:00
|
|
|
|
if ( $pat[$i] <= $pat[$i + 2] ) {
|
|
|
|
|
$re .= preg_quote( $pat[$i], '/' ) . '-' . preg_quote( $pat[$i + 2], '/' );
|
2015-04-17 15:02:48 +00:00
|
|
|
|
}
|
2013-04-19 19:26:45 +00:00
|
|
|
|
$i += 2;
|
|
|
|
|
} else {
|
|
|
|
|
$re .= preg_quote( $pat[$i], '/' );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ( $i >= $len ) {
|
2016-05-17 14:52:05 +00:00
|
|
|
|
throw new Scribunto_LuaError(
|
|
|
|
|
"Missing close-bracket for character set beginning at pattern character $ii"
|
|
|
|
|
);
|
2013-04-19 19:26:45 +00:00
|
|
|
|
}
|
|
|
|
|
$re .= ']';
|
2015-04-17 15:02:48 +00:00
|
|
|
|
|
|
|
|
|
// Lua just ignores invalid ranges, while pcre throws an error.
|
|
|
|
|
// We filter them out above, but then we need to special-case empty sets
|
|
|
|
|
if ( $re === '[]' ) {
|
|
|
|
|
// Can't directly quantify (*FAIL), so wrap it.
|
|
|
|
|
// "(?!)" would be simpler and could be quantified if not for a bug in PCRE 8.13 to 8.33
|
|
|
|
|
$re = '(?:(*FAIL))';
|
|
|
|
|
} elseif ( $re === '[^]' ) {
|
|
|
|
|
$re = '.'; // 's' modifier is always used, so this works
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ $i, $re ];
|
2013-04-19 19:26:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-14 18:50:34 +00:00
|
|
|
|
/**
|
|
|
|
|
* Append captured groups to a result array
|
|
|
|
|
* @param array $arr Result array to append to.
|
|
|
|
|
* @param string $s String matched against.
|
|
|
|
|
* @param array $m Matches, from preg_match with PREG_OFFSET_CAPTURE.
|
|
|
|
|
* @param array $capt Capture groups (in $m) to process, see patternToRegex()
|
|
|
|
|
* @param bool $m0_if_no_captures Whether to append "$0" if $capt is empty.
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
2013-04-19 19:26:45 +00:00
|
|
|
|
private function addCapturesFromMatch( $arr, $s, $m, $capt, $m0_if_no_captures ) {
|
2012-12-11 02:53:43 +00:00
|
|
|
|
if ( count( $capt ) ) {
|
|
|
|
|
foreach ( $capt as $n => $pos ) {
|
|
|
|
|
if ( $pos ) {
|
2013-04-19 19:26:45 +00:00
|
|
|
|
$o = mb_strlen( substr( $s, 0, $m["m$n"][1] ), 'UTF-8' ) + 1;
|
2012-12-11 02:53:43 +00:00
|
|
|
|
$arr[] = $o;
|
|
|
|
|
} else {
|
|
|
|
|
$arr[] = $m["m$n"][0];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $m0_if_no_captures ) {
|
|
|
|
|
$arr[] = $m[0][0];
|
|
|
|
|
}
|
|
|
|
|
return $arr;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for find
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @param string $pattern
|
|
|
|
|
* @param int $init
|
|
|
|
|
* @param bool $plain
|
|
|
|
|
* @return array Format is [ null ], or [ int, int ], or [ int, int, (string|int)... ]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringFind( $s, $pattern, $init = 1, $plain = false ) {
|
|
|
|
|
$this->checkString( 'find', $s );
|
|
|
|
|
$this->checkTypeOptional( 'find', 3, $init, 'number', 1 );
|
|
|
|
|
$this->checkTypeOptional( 'find', 4, $plain, 'boolean', false );
|
|
|
|
|
|
|
|
|
|
$len = mb_strlen( $s, 'UTF-8' );
|
|
|
|
|
if ( $init < 0 ) {
|
|
|
|
|
$init = $len + $init + 1;
|
2013-04-18 17:51:07 +00:00
|
|
|
|
} elseif ( $init > $len + 1 ) {
|
|
|
|
|
$init = $len + 1;
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $init > 1 ) {
|
2013-04-19 19:26:45 +00:00
|
|
|
|
$offset = strlen( mb_substr( $s, 0, $init - 1, 'UTF-8' ) );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
} else {
|
|
|
|
|
$init = 1;
|
2013-04-19 19:26:45 +00:00
|
|
|
|
$offset = 0;
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $plain ) {
|
2014-07-11 19:39:32 +00:00
|
|
|
|
$this->checkPattern( 'find', $pattern );
|
2013-04-18 17:51:07 +00:00
|
|
|
|
if ( $pattern !== '' ) {
|
2013-04-19 19:26:45 +00:00
|
|
|
|
$ret = mb_strpos( $s, $pattern, $init - 1, 'UTF-8' );
|
2013-04-18 17:51:07 +00:00
|
|
|
|
} else {
|
2013-04-19 19:26:45 +00:00
|
|
|
|
$ret = $init - 1;
|
2013-04-18 17:51:07 +00:00
|
|
|
|
}
|
2013-03-05 01:14:05 +00:00
|
|
|
|
if ( $ret === false ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ null ];
|
2013-03-05 01:14:05 +00:00
|
|
|
|
} else {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ $ret + 1, $ret + mb_strlen( $pattern ) ];
|
2013-03-05 01:14:05 +00:00
|
|
|
|
}
|
2014-07-11 19:39:32 +00:00
|
|
|
|
} else {
|
|
|
|
|
list( $re, $capt ) = $this->patternToRegex( $pattern, '\G', 'find' );
|
|
|
|
|
if ( !preg_match( $re, $s, $m, PREG_OFFSET_CAPTURE, $offset ) ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ null ];
|
2014-07-11 19:39:32 +00:00
|
|
|
|
}
|
|
|
|
|
$o = mb_strlen( substr( $s, 0, $m[0][1] ), 'UTF-8' );
|
2017-06-15 17:19:00 +00:00
|
|
|
|
$ret = [ $o + 1, $o + mb_strlen( $m[0][0], 'UTF-8' ) ];
|
2014-07-11 19:39:32 +00:00
|
|
|
|
return $this->addCapturesFromMatch( $ret, $s, $m, $capt, false );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for match
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @param string $pattern
|
|
|
|
|
* @param int $init
|
|
|
|
|
* @return array Format is [ null ] or [ (string|int)... ]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringMatch( $s, $pattern, $init = 1 ) {
|
|
|
|
|
$this->checkString( 'match', $s );
|
|
|
|
|
$this->checkTypeOptional( 'match', 3, $init, 'number', 1 );
|
|
|
|
|
|
|
|
|
|
$len = mb_strlen( $s, 'UTF-8' );
|
|
|
|
|
if ( $init < 0 ) {
|
|
|
|
|
$init = $len + $init + 1;
|
2013-04-18 17:51:07 +00:00
|
|
|
|
} elseif ( $init > $len + 1 ) {
|
|
|
|
|
$init = $len + 1;
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
if ( $init > 1 ) {
|
2013-04-19 19:26:45 +00:00
|
|
|
|
$offset = strlen( mb_substr( $s, 0, $init - 1, 'UTF-8' ) );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
} else {
|
2013-04-19 19:26:45 +00:00
|
|
|
|
$offset = 0;
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2014-07-11 19:39:32 +00:00
|
|
|
|
list( $re, $capt ) = $this->patternToRegex( $pattern, '\G', 'match' );
|
2013-04-19 19:26:45 +00:00
|
|
|
|
if ( !preg_match( $re, $s, $m, PREG_OFFSET_CAPTURE, $offset ) ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ null ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return $this->addCapturesFromMatch( [], $s, $m, $capt, true );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for gmatchInit
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @param string $pattern
|
|
|
|
|
* @return array Format is [ string, bool[] ]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringGmatchInit( $s, $pattern ) {
|
|
|
|
|
$this->checkString( 'gmatch', $s );
|
|
|
|
|
|
2014-07-11 19:39:32 +00:00
|
|
|
|
list( $re, $capt ) = $this->patternToRegex( $pattern, false, 'gmatch' );
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ $re, $capt ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for gmatchCallback
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @param string $re
|
|
|
|
|
* @param bool[] $capt
|
|
|
|
|
* @param int $pos
|
|
|
|
|
* @return array Format is [ int, [ null, (string|int)... ] ]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringGmatchCallback( $s, $re, $capt, $pos ) {
|
|
|
|
|
if ( !preg_match( $re, $s, $m, PREG_OFFSET_CAPTURE, $pos ) ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ $pos, [] ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
$pos = $m[0][1] + strlen( $m[0][0] );
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ $pos, $this->addCapturesFromMatch( [ null ], $s, $m, $capt, true ) ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 19:31:08 +00:00
|
|
|
|
/**
|
|
|
|
|
* Handler for gsub
|
|
|
|
|
* @internal
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @param string $pattern
|
|
|
|
|
* @param mixed $repl
|
|
|
|
|
* @param string|int|null $n
|
|
|
|
|
* @return array Format is [ string, int ]
|
|
|
|
|
*/
|
2012-12-11 02:53:43 +00:00
|
|
|
|
public function ustringGsub( $s, $pattern, $repl, $n = null ) {
|
|
|
|
|
$this->checkString( 'gsub', $s );
|
|
|
|
|
$this->checkTypeOptional( 'gsub', 4, $n, 'number', null );
|
|
|
|
|
|
|
|
|
|
if ( $n === null ) {
|
|
|
|
|
$n = -1;
|
2015-09-23 17:31:54 +00:00
|
|
|
|
} elseif ( $n < 1 ) {
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ $s, 0 ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2014-07-11 19:39:32 +00:00
|
|
|
|
list( $re, $capt, $anypos ) = $this->patternToRegex( $pattern, '^', 'gsub' );
|
2017-06-15 17:19:00 +00:00
|
|
|
|
$captures = [];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
|
2015-09-23 17:31:54 +00:00
|
|
|
|
if ( $this->phpBug53823 ) {
|
|
|
|
|
// PHP bug 53823 means that a zero-length match before a UTF-8
|
|
|
|
|
// character will match again before every byte of that character.
|
|
|
|
|
// The workaround is to capture the first "character" of/after the
|
|
|
|
|
// match and verify that its first byte is legal to start a UTF-8
|
|
|
|
|
// character.
|
|
|
|
|
$re = '/(?=(?<phpBug53823>.|$))' . substr( $re, 1 );
|
|
|
|
|
}
|
|
|
|
|
|
2012-12-11 02:53:43 +00:00
|
|
|
|
if ( $anypos ) {
|
|
|
|
|
// preg_replace_callback doesn't take a "flags" argument, so we
|
|
|
|
|
// can't pass PREG_OFFSET_CAPTURE to it, which is needed to handle
|
|
|
|
|
// position captures. So instead we have to do a preg_match_all and
|
|
|
|
|
// handle the captures ourself.
|
|
|
|
|
$ct = preg_match_all( $re, $s, $mm, PREG_OFFSET_CAPTURE | PREG_SET_ORDER );
|
|
|
|
|
for ( $i = 0; $i < $ct; $i++ ) {
|
|
|
|
|
$m = $mm[$i];
|
2015-09-23 17:31:54 +00:00
|
|
|
|
if ( $this->phpBug53823 ) {
|
|
|
|
|
$c = ord( $m['phpBug53823'][0] );
|
|
|
|
|
if ( $c >= 0x80 && $c <= 0xbf ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-06-15 17:19:00 +00:00
|
|
|
|
$c = [ $m[0][0] ];
|
|
|
|
|
foreach ( $this->addCapturesFromMatch( [], $s, $m, $capt, false ) as $k => $v ) {
|
2012-12-11 02:53:43 +00:00
|
|
|
|
$k++;
|
|
|
|
|
$c["m$k"] = $v;
|
|
|
|
|
}
|
|
|
|
|
$captures[] = $c;
|
2015-09-23 17:31:54 +00:00
|
|
|
|
if ( $n >= 0 && count( $captures ) >= $n ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch ( $this->getLuaType( $repl ) ) {
|
|
|
|
|
case 'string':
|
2014-06-27 16:31:04 +00:00
|
|
|
|
case 'number':
|
2012-12-11 02:53:43 +00:00
|
|
|
|
$cb = function ( $m ) use ( $repl, $anypos, &$captures ) {
|
|
|
|
|
if ( $anypos ) {
|
|
|
|
|
$m = array_shift( $captures );
|
|
|
|
|
}
|
|
|
|
|
return preg_replace_callback( '/%([%0-9])/', function ( $m2 ) use ( $m ) {
|
|
|
|
|
$x = $m2[1];
|
|
|
|
|
if ( $x === '%' ) {
|
|
|
|
|
return '%';
|
|
|
|
|
} elseif ( $x === '0' ) {
|
|
|
|
|
return $m[0];
|
|
|
|
|
} elseif ( isset( $m["m$x"] ) ) {
|
|
|
|
|
return $m["m$x"];
|
2018-10-22 14:49:17 +00:00
|
|
|
|
} elseif ( $x === '1' ) {
|
|
|
|
|
// Match undocumented Lua string.gsub behavior
|
|
|
|
|
return $m[0];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
} else {
|
|
|
|
|
throw new Scribunto_LuaError( "invalid capture index %$x in replacement string" );
|
|
|
|
|
}
|
|
|
|
|
}, $repl );
|
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'table':
|
|
|
|
|
$cb = function ( $m ) use ( $repl, $anypos, &$captures ) {
|
|
|
|
|
if ( $anypos ) {
|
|
|
|
|
$m = array_shift( $captures );
|
|
|
|
|
}
|
2019-03-21 04:16:08 +00:00
|
|
|
|
$x = $m['m1'] ?? $m[0];
|
2018-05-22 22:50:00 +00:00
|
|
|
|
if ( !isset( $repl[$x] ) || $repl[$x] === null ) {
|
|
|
|
|
return $m[0];
|
|
|
|
|
}
|
|
|
|
|
$type = $this->getLuaType( $repl[$x] );
|
|
|
|
|
if ( $type !== 'string' && $type !== 'number' ) {
|
|
|
|
|
throw new Scribunto_LuaError( "invalid replacement value (a $type)" );
|
|
|
|
|
}
|
|
|
|
|
return $repl[$x];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'function':
|
|
|
|
|
$interpreter = $this->getInterpreter();
|
|
|
|
|
$cb = function ( $m ) use ( $interpreter, $capt, $repl, $anypos, &$captures ) {
|
|
|
|
|
if ( $anypos ) {
|
|
|
|
|
$m = array_shift( $captures );
|
|
|
|
|
}
|
2018-06-08 07:56:03 +00:00
|
|
|
|
$args = [];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
if ( count( $capt ) ) {
|
|
|
|
|
foreach ( $capt as $i => $pos ) {
|
2020-12-11 06:46:04 +00:00
|
|
|
|
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
|
2012-12-11 02:53:43 +00:00
|
|
|
|
$args[] = $m["m$i"];
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$args[] = $m[0];
|
|
|
|
|
}
|
2018-06-08 07:56:03 +00:00
|
|
|
|
$ret = $interpreter->callFunction( $repl, ...$args );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
if ( count( $ret ) === 0 || $ret[0] === null ) {
|
|
|
|
|
return $m[0];
|
|
|
|
|
}
|
2018-05-22 22:50:00 +00:00
|
|
|
|
$type = $this->getLuaType( $ret[0] );
|
|
|
|
|
if ( $type !== 'string' && $type !== 'number' ) {
|
|
|
|
|
throw new Scribunto_LuaError( "invalid replacement value (a $type)" );
|
|
|
|
|
}
|
2012-12-11 02:53:43 +00:00
|
|
|
|
return $ret[0];
|
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
$this->checkType( 'gsub', 3, $repl, 'function or table or string' );
|
2020-01-30 03:53:43 +00:00
|
|
|
|
throw new LogicException( 'checkType above should have failed' );
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2015-09-23 17:31:54 +00:00
|
|
|
|
$skippedMatches = 0;
|
|
|
|
|
if ( $this->phpBug53823 ) {
|
|
|
|
|
// Since we're having bogus matches, we need to keep track of the
|
|
|
|
|
// necessary adjustment and stop manually once we hit the limit.
|
|
|
|
|
$maxMatches = $n < 0 ? INF : $n;
|
|
|
|
|
$n = -1;
|
|
|
|
|
$realCallback = $cb;
|
|
|
|
|
$cb = function ( $m ) use ( $realCallback, &$skippedMatches, &$maxMatches ) {
|
|
|
|
|
$c = ord( $m['phpBug53823'] );
|
|
|
|
|
if ( $c >= 0x80 && $c <= 0xbf || $maxMatches <= 0 ) {
|
|
|
|
|
$skippedMatches++;
|
|
|
|
|
return $m[0];
|
|
|
|
|
} else {
|
|
|
|
|
$maxMatches--;
|
|
|
|
|
return $realCallback( $m );
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2012-12-11 02:53:43 +00:00
|
|
|
|
$count = 0;
|
|
|
|
|
$s2 = preg_replace_callback( $re, $cb, $s, $n, $count );
|
2016-03-24 14:08:29 +00:00
|
|
|
|
if ( $s2 === null ) {
|
|
|
|
|
self::handlePCREError( preg_last_error(), $pattern );
|
|
|
|
|
}
|
2017-06-15 17:19:00 +00:00
|
|
|
|
return [ $s2, $count - $skippedMatches ];
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|
2016-03-24 14:08:29 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle a PCRE error
|
|
|
|
|
* @param int $error From preg_last_error()
|
|
|
|
|
* @param string $pattern Pattern being matched
|
|
|
|
|
* @throws Scribunto_LuaError
|
|
|
|
|
*/
|
|
|
|
|
private function handlePCREError( $error, $pattern ) {
|
|
|
|
|
$PREG_JIT_STACKLIMIT_ERROR = defined( 'PREG_JIT_STACKLIMIT_ERROR' )
|
|
|
|
|
? PREG_JIT_STACKLIMIT_ERROR
|
|
|
|
|
: 'PREG_JIT_STACKLIMIT_ERROR';
|
|
|
|
|
|
|
|
|
|
$error = preg_last_error();
|
|
|
|
|
switch ( $error ) {
|
|
|
|
|
case PREG_NO_ERROR:
|
|
|
|
|
// Huh?
|
|
|
|
|
break;
|
|
|
|
|
case PREG_INTERNAL_ERROR:
|
|
|
|
|
throw new Scribunto_LuaError( "PCRE internal error" );
|
|
|
|
|
case PREG_BACKTRACK_LIMIT_ERROR:
|
|
|
|
|
throw new Scribunto_LuaError(
|
|
|
|
|
"PCRE backtrack limit reached while matching pattern '$pattern'"
|
|
|
|
|
);
|
|
|
|
|
case PREG_RECURSION_LIMIT_ERROR:
|
|
|
|
|
throw new Scribunto_LuaError(
|
|
|
|
|
"PCRE recursion limit reached while matching pattern '$pattern'"
|
|
|
|
|
);
|
|
|
|
|
case PREG_BAD_UTF8_ERROR:
|
|
|
|
|
// Should have alreay been caught, but just in case
|
|
|
|
|
throw new Scribunto_LuaError( "PCRE bad UTF-8 error" );
|
|
|
|
|
case PREG_BAD_UTF8_OFFSET_ERROR:
|
|
|
|
|
// Shouldn't happen, but just in case
|
|
|
|
|
throw new Scribunto_LuaError( "PCRE bad UTF-8 offset error" );
|
|
|
|
|
case $PREG_JIT_STACKLIMIT_ERROR:
|
|
|
|
|
throw new Scribunto_LuaError(
|
|
|
|
|
"PCRE JIT stack limit reached while matching pattern '$pattern'"
|
|
|
|
|
);
|
|
|
|
|
default:
|
|
|
|
|
throw new Scribunto_LuaError(
|
|
|
|
|
"PCRE error code $error while matching pattern '$pattern'"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2012-12-11 02:53:43 +00:00
|
|
|
|
}
|