Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4114473
UstringLibrary.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
24 KB
Referenced Files
None
Subscribers
None
UstringLibrary.php
View Options
<?php
namespace
MediaWiki\Extension\Scribunto\Engines\LuaCommon
;
use
LogicException
;
use
MapCacheLRU
;
use
MediaWiki\MainConfigNames
;
use
MediaWiki\MediaWikiServices
;
use
UtfNormal\Validator
;
class
UstringLibrary
extends
LibraryBase
{
/**
* Limit on pattern lengths, in bytes not characters
* @var int
*/
private
$patternLengthLimit
=
10000
;
/**
* Limit on string lengths, in bytes not characters
* @var int
*/
private
$stringLengthLimit
;
/**
* A cache of patterns and the regexes they generate.
* @var MapCacheLRU
*/
private
$patternRegexCache
;
/** @inheritDoc */
public
function
__construct
(
$engine
)
{
$maxArticleSize
=
MediaWikiServices
::
getInstance
()->
getMainConfig
()->
get
(
MainConfigNames
::
MaxArticleSize
);
$this
->
stringLengthLimit
=
$maxArticleSize
*
1024
;
$this
->
patternRegexCache
=
new
MapCacheLRU
(
100
);
parent
::
__construct
(
$engine
);
}
/** @inheritDoc */
public
function
register
()
{
$perf
=
$this
->
getEngine
()->
getPerformanceCharacteristics
();
if
(
$perf
[
'phpCallsRequireSerialization'
]
)
{
$lib
=
[
// Pattern matching is still much faster in PHP, even with the
// overhead of serialization
'find'
=>
[
$this
,
'ustringFind'
],
'match'
=>
[
$this
,
'ustringMatch'
],
'gmatch_init'
=>
[
$this
,
'ustringGmatchInit'
],
'gmatch_callback'
=>
[
$this
,
'ustringGmatchCallback'
],
'gsub'
=>
[
$this
,
'ustringGsub'
],
];
}
else
{
$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
,
[
'stringLengthLimit'
=>
$this
->
stringLengthLimit
,
'patternLengthLimit'
=>
$this
->
patternLengthLimit
,
]
);
}
/**
* 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.
*/
private
function
checkString
(
$name
,
&
$s
,
$checkEncoding
=
true
)
{
if
(
$this
->
getLuaType
(
$s
)
==
'number'
)
{
$s
=
(
string
)
$s
;
}
else
{
$this
->
checkType
(
$name
,
1
,
$s
,
'string'
);
if
(
$checkEncoding
&&
!
mb_check_encoding
(
$s
,
'UTF-8'
)
)
{
throw
new
LuaError
(
"bad argument #1 to '$name' (string is not UTF-8)"
);
}
if
(
strlen
(
$s
)
>
$this
->
stringLengthLimit
)
{
throw
new
LuaError
(
"bad argument #1 to '$name' (string is longer than $this->stringLengthLimit bytes)"
);
}
}
}
/**
* Handler for isUtf8
* @internal
* @param string $s
* @return bool[]
*/
public
function
ustringIsUtf8
(
$s
)
{
$this
->
checkString
(
'isutf8'
,
$s
,
false
);
return
[
mb_check_encoding
(
$s
,
'UTF-8'
)
];
}
/**
* Handler for byteoffset
* @internal
* @param string $s
* @param int $l
* @param int $i
* @return int[]|null[]
*/
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
)
{
$i
=
$bytelen
+
$i
+
1
;
}
if
(
$i
<
1
||
$i
>
$bytelen
)
{
return
[
null
];
}
$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'
)
)
{
return
[
null
];
}
else
{
return
[
strlen
(
mb_substr
(
$s
,
0
,
$char
,
'UTF-8'
)
)
+
1
];
}
}
/**
* Handler for codepoint
* @internal
* @param string $s
* @param int $i
* @param int|null $j
* @return int[]
*/
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
;
}
if
(
$j
<
$i
)
{
return
[];
}
$i
=
max
(
1
,
min
(
$i
,
$l
+
1
)
);
$j
=
max
(
1
,
min
(
$j
,
$l
+
1
)
);
$s
=
mb_substr
(
$s
,
$i
-
1
,
$j
-
$i
+
1
,
'UTF-8'
);
return
unpack
(
'N*'
,
mb_convert_encoding
(
$s
,
'UTF-32BE'
,
'UTF-8'
)
);
}
/**
* Handler for gcodepointInit
* @internal
* @param string $s
* @param int $i
* @param int|null $j
* @return int[][]
*/
public
function
ustringGcodepointInit
(
$s
,
$i
=
1
,
$j
=
null
)
{
return
[
$this
->
ustringCodepoint
(
$s
,
$i
,
$j
)
];
}
/**
* Handler for toNFC
* @internal
* @param string $s
* @return string[]|null[]
*/
public
function
ustringToNFC
(
$s
)
{
$this
->
checkString
(
'toNFC'
,
$s
,
false
);
if
(
!
mb_check_encoding
(
$s
,
'UTF-8'
)
)
{
return
[
null
];
}
return
[
Validator
::
toNFC
(
$s
)
];
}
/**
* Handler for toNFD
* @internal
* @param string $s
* @return string[]|null[]
*/
public
function
ustringToNFD
(
$s
)
{
$this
->
checkString
(
'toNFD'
,
$s
,
false
);
if
(
!
mb_check_encoding
(
$s
,
'UTF-8'
)
)
{
return
[
null
];
}
return
[
Validator
::
toNFD
(
$s
)
];
}
/**
* Handler for toNFKC
* @internal
* @param string $s
* @return string[]|null[]
*/
public
function
ustringToNFKC
(
$s
)
{
$this
->
checkString
(
'toNFKC'
,
$s
,
false
);
if
(
!
mb_check_encoding
(
$s
,
'UTF-8'
)
)
{
return
[
null
];
}
return
[
Validator
::
toNFKC
(
$s
)
];
}
/**
* Handler for toNFKD
* @internal
* @param string $s
* @return string[]|null[]
*/
public
function
ustringToNFKD
(
$s
)
{
$this
->
checkString
(
'toNFKD'
,
$s
,
false
);
if
(
!
mb_check_encoding
(
$s
,
'UTF-8'
)
)
{
return
[
null
];
}
return
[
Validator
::
toNFKD
(
$s
)
];
}
/**
* Handler for char
* @internal
* @param int ...$args
* @return string[]
*/
public
function
ustringChar
(
...
$args
)
{
if
(
count
(
$args
)
>
$this
->
stringLengthLimit
)
{
throw
new
LuaError
(
"too many arguments to 'char'"
);
}
foreach
(
$args
as
$k
=>
&
$v
)
{
if
(
!
is_numeric
(
$v
)
)
{
$this
->
checkType
(
'char'
,
$k
+
1
,
$v
,
'number'
);
}
$v
=
(
int
)
floor
(
$v
);
if
(
$v
<
0
||
$v
>
0x10ffff
)
{
$k
++;
throw
new
LuaError
(
"bad argument #$k to 'char' (value out of range)"
);
}
}
$s
=
pack
(
'N*'
,
...
$args
);
$s
=
mb_convert_encoding
(
$s
,
'UTF-8'
,
'UTF-32BE'
);
if
(
strlen
(
$s
)
>
$this
->
stringLengthLimit
)
{
throw
new
LuaError
(
"result to long for 'char'"
);
}
return
[
$s
];
}
/**
* Handler for len
* @internal
* @param string $s
* @return int[]|null[]
*/
public
function
ustringLen
(
$s
)
{
$this
->
checkString
(
'len'
,
$s
,
false
);
if
(
!
mb_check_encoding
(
$s
,
'UTF-8'
)
)
{
return
[
null
];
}
return
[
mb_strlen
(
$s
,
'UTF-8'
)
];
}
/**
* Handler for sub
* @internal
* @param string $s
* @param int $i
* @param int $j
* @return string[]
*/
public
function
ustringSub
(
$s
,
$i
=
1
,
$j
=
-
1
)
{
$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
;
}
if
(
$j
<
$i
)
{
return
[
''
];
}
$i
=
max
(
1
,
min
(
(
int
)
$i
,
$len
+
1
)
);
$j
=
max
(
1
,
min
(
(
int
)
$j
,
$len
+
1
)
);
$s
=
mb_substr
(
$s
,
$i
-
1
,
$j
-
$i
+
1
,
'UTF-8'
);
return
[
$s
];
}
/**
* Handler for upper
* @internal
* @param string $s
* @return string[]
*/
public
function
ustringUpper
(
$s
)
{
$this
->
checkString
(
'upper'
,
$s
);
return
[
mb_strtoupper
(
$s
,
'UTF-8'
)
];
}
/**
* Handler for lower
* @internal
* @param string $s
* @return string[]
*/
public
function
ustringLower
(
$s
)
{
$this
->
checkString
(
'lower'
,
$s
);
return
[
mb_strtolower
(
$s
,
'UTF-8'
)
];
}
/**
* Check a pattern as the second argument
* @param string $name Lua function name, for errors
* @param mixed $pattern Lua pattern
*/
private
function
checkPattern
(
$name
,
$pattern
)
{
if
(
$this
->
getLuaType
(
$pattern
)
==
'number'
)
{
$pattern
=
(
string
)
$pattern
;
}
$this
->
checkType
(
$name
,
2
,
$pattern
,
'string'
);
if
(
!
mb_check_encoding
(
$pattern
,
'UTF-8'
)
)
{
throw
new
LuaError
(
"bad argument #2 to '$name' (string is not UTF-8)"
);
}
if
(
strlen
(
$pattern
)
>
$this
->
patternLengthLimit
)
{
throw
new
LuaError
(
"bad argument #2 to '$name' (pattern is longer than $this->patternLengthLimit bytes)"
);
}
}
/**
* 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.
* @return-taint none
*/
private
function
patternToRegex
(
$pattern
,
$anchor
,
$name
)
{
$cacheKey
=
serialize
(
[
$pattern
,
$anchor
]
);
if
(
!
$this
->
patternRegexCache
->
has
(
$cacheKey
)
)
{
$this
->
checkPattern
(
$name
,
$pattern
);
$pat
=
preg_split
(
'//us'
,
$pattern
,
-
1
,
PREG_SPLIT_NO_EMPTY
);
static
$charsets
=
null
,
$brcharsets
=
null
;
if
(
$charsets
===
null
)
{
$charsets
=
[
// 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
]'
,
];
$brcharsets
=
[
'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'
=>
'
\x
00-
\x
2f
\x
3a-
\x
40
\x
47-
\x
60
\x
67-
\x
{ff0f}'
.
'
\x
{ff1a}-
\x
{ff20}
\x
{ff27}-
\x
{ff40}
\x
{ff47}-
\x
{10ffff}'
,
// Ha!
'Z'
=>
'
\x
01-
\x
{10ffff}'
,
]
+
$charsets
;
}
$re
=
'/'
;
$len
=
count
(
$pat
);
$capt
=
[];
$anypos
=
false
;
$captparen
=
[];
$opencapt
=
[];
$bct
=
0
;
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
LuaError
(
"Unmatched open-paren at pattern character $ii"
);
}
$n
=
count
(
$capt
)
+
1
;
$capt
[
$n
]
=
(
$pat
[
$i
+
1
]
===
')'
);
if
(
$capt
[
$n
]
)
{
$anypos
=
true
;
}
$re
.=
"(?<m$n>"
;
$opencapt
[]
=
$n
;
$captparen
[
$n
]
=
$ii
;
break
;
case
')'
:
if
(
count
(
$opencapt
)
<=
0
)
{
throw
new
LuaError
(
"Unmatched close-paren at pattern character $ii"
);
}
array_pop
(
$opencapt
);
$re
.=
$pat
[
$i
];
break
;
case
'%'
:
$i
++;
if
(
$i
>=
$len
)
{
throw
new
LuaError
(
"malformed pattern (ends with '%')"
);
}
if
(
isset
(
$charsets
[
$pat
[
$i
]]
)
)
{
$re
.=
$charsets
[
$pat
[
$i
]];
$q
=
true
;
}
elseif
(
$pat
[
$i
]
===
'b'
)
{
if
(
$i
+
2
>=
$len
)
{
throw
new
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
LuaError
(
"missing '[' after %f in pattern at pattern character $ii"
);
}
[
$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 "$".
// @phan-suppress-next-line PhanParamSuspiciousOrder
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
LuaError
(
"invalid capture index %$n at pattern character $ii"
);
}
$re
.=
"
\\
g{m$n}"
;
}
else
{
$re
.=
preg_quote
(
$pat
[
$i
],
'/'
);
$q
=
true
;
}
break
;
case
'['
:
[
$i
,
$re2
]
=
$this
->
bracketedCharSetToRegex
(
$pat
,
$i
,
$len
,
$brcharsets
);
$re
.=
$re2
;
$q
=
true
;
break
;
case
']'
:
throw
new
LuaError
(
"Unmatched close-bracket at pattern character $ii"
);
case
'.'
:
$re
.=
$pat
[
$i
];
$q
=
true
;
break
;
default
:
$re
.=
preg_quote
(
$pat
[
$i
],
'/'
);
$q
=
true
;
break
;
}
if
(
$q
&&
$i
+
1
<
$len
)
{
switch
(
$pat
[
$i
+
1
]
)
{
case
'*'
:
case
'+'
:
case
'?'
:
$re
.=
$pat
[++
$i
];
break
;
case
'-'
:
$re
.=
'*?'
;
$i
++;
break
;
}
}
}
if
(
count
(
$opencapt
)
)
{
$ii
=
$captparen
[
$opencapt
[
0
]];
throw
new
LuaError
(
"Unclosed capture beginning at pattern character $ii"
);
}
$re
.=
'/us'
;
$this
->
patternRegexCache
->
set
(
$cacheKey
,
[
$re
,
$capt
,
$anypos
]
);
}
return
$this
->
patternRegexCache
->
get
(
$cacheKey
);
}
/**
* 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 ]
*/
private
function
bracketedCharSetToRegex
(
$pat
,
$i
,
$len
,
$brcharsets
)
{
$ii
=
$i
+
1
;
$re
=
'['
;
$i
++;
if
(
$i
<
$len
&&
$pat
[
$i
]
===
'^'
)
{
$re
.=
'^'
;
$i
++;
}
for
(
$j
=
$i
;
$i
<
$len
&&
(
$j
==
$i
||
$pat
[
$i
]
!==
']'
);
$i
++
)
{
if
(
$pat
[
$i
]
===
'%'
)
{
$i
++;
if
(
$i
>=
$len
)
{
break
;
}
$re
.=
$brcharsets
[
$pat
[
$i
]]
??
preg_quote
(
$pat
[
$i
],
'/'
);
}
elseif
(
$i
+
2
<
$len
&&
$pat
[
$i
+
1
]
===
'-'
&&
$pat
[
$i
+
2
]
!==
']'
&&
$pat
[
$i
+
2
]
!==
'%'
)
{
if
(
$pat
[
$i
]
<=
$pat
[
$i
+
2
]
)
{
$re
.=
preg_quote
(
$pat
[
$i
],
'/'
)
.
'-'
.
preg_quote
(
$pat
[
$i
+
2
],
'/'
);
}
$i
+=
2
;
}
else
{
$re
.=
preg_quote
(
$pat
[
$i
],
'/'
);
}
}
if
(
$i
>=
$len
)
{
throw
new
LuaError
(
"Missing close-bracket for character set beginning at pattern character $ii"
);
}
$re
.=
']'
;
// 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
===
'[^]'
)
{
// 's' modifier is always used, so this works
$re
=
'.'
;
}
return
[
$i
,
$re
];
}
/**
* 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
*/
private
function
addCapturesFromMatch
(
$arr
,
$s
,
$m
,
$capt
,
$m0_if_no_captures
)
{
if
(
count
(
$capt
)
)
{
foreach
(
$capt
as
$n
=>
$pos
)
{
if
(
$pos
)
{
$o
=
mb_strlen
(
substr
(
$s
,
0
,
$m
[
"m$n"
][
1
]
),
'UTF-8'
)
+
1
;
$arr
[]
=
$o
;
}
else
{
$arr
[]
=
$m
[
"m$n"
][
0
];
}
}
}
elseif
(
$m0_if_no_captures
)
{
$arr
[]
=
$m
[
0
][
0
];
}
return
$arr
;
}
/**
* 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)... ]
*/
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
;
}
elseif
(
$init
>
$len
+
1
)
{
$init
=
$len
+
1
;
}
if
(
$init
>
1
)
{
$offset
=
strlen
(
mb_substr
(
$s
,
0
,
$init
-
1
,
'UTF-8'
)
);
}
else
{
$init
=
1
;
$offset
=
0
;
}
if
(
$plain
)
{
$this
->
checkPattern
(
'find'
,
$pattern
);
if
(
$pattern
!==
''
)
{
$ret
=
mb_strpos
(
$s
,
$pattern
,
$init
-
1
,
'UTF-8'
);
}
else
{
$ret
=
$init
-
1
;
}
if
(
$ret
===
false
)
{
return
[
null
];
}
else
{
return
[
$ret
+
1
,
$ret
+
mb_strlen
(
$pattern
)
];
}
}
else
{
[
$re
,
$capt
]
=
$this
->
patternToRegex
(
$pattern
,
'
\G
'
,
'find'
);
if
(
!
preg_match
(
$re
,
$s
,
$m
,
PREG_OFFSET_CAPTURE
,
$offset
)
)
{
return
[
null
];
}
$o
=
mb_strlen
(
substr
(
$s
,
0
,
$m
[
0
][
1
]
),
'UTF-8'
);
$ret
=
[
$o
+
1
,
$o
+
mb_strlen
(
$m
[
0
][
0
],
'UTF-8'
)
];
return
$this
->
addCapturesFromMatch
(
$ret
,
$s
,
$m
,
$capt
,
false
);
}
}
/**
* Handler for match
* @internal
* @param string $s
* @param string $pattern
* @param int $init
* @return array Format is [ null ] or [ (string|int)... ]
*/
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
;
}
elseif
(
$init
>
$len
+
1
)
{
$init
=
$len
+
1
;
}
if
(
$init
>
1
)
{
$offset
=
strlen
(
mb_substr
(
$s
,
0
,
$init
-
1
,
'UTF-8'
)
);
}
else
{
$offset
=
0
;
}
[
$re
,
$capt
]
=
$this
->
patternToRegex
(
$pattern
,
'
\G
'
,
'match'
);
if
(
!
preg_match
(
$re
,
$s
,
$m
,
PREG_OFFSET_CAPTURE
,
$offset
)
)
{
return
[
null
];
}
return
$this
->
addCapturesFromMatch
(
[],
$s
,
$m
,
$capt
,
true
);
}
/**
* Handler for gmatchInit
* @internal
* @param string $s
* @param string $pattern
* @return array Format is [ string, bool[] ]
*/
public
function
ustringGmatchInit
(
$s
,
$pattern
)
{
$this
->
checkString
(
'gmatch'
,
$s
);
[
$re
,
$capt
]
=
$this
->
patternToRegex
(
$pattern
,
false
,
'gmatch'
);
return
[
$re
,
$capt
];
}
/**
* Handler for gmatchCallback
* @internal
* @param string $s
* @param string $re
* @param bool[] $capt
* @param int $pos
* @return array Format is [ int, [ null, (string|int)... ] ]
*/
public
function
ustringGmatchCallback
(
$s
,
$re
,
$capt
,
$pos
)
{
if
(
!
preg_match
(
$re
,
$s
,
$m
,
PREG_OFFSET_CAPTURE
,
$pos
)
)
{
return
[
$pos
,
[]
];
}
$pos
=
$m
[
0
][
1
]
+
strlen
(
$m
[
0
][
0
]
);
return
[
$pos
,
$this
->
addCapturesFromMatch
(
[
null
],
$s
,
$m
,
$capt
,
true
)
];
}
/**
* Handler for gsub
* @internal
* @param string $s
* @param string $pattern
* @param mixed $repl
* @param string|int|null $n
* @return array Format is [ string, int ]
*/
public
function
ustringGsub
(
$s
,
$pattern
,
$repl
,
$n
=
null
)
{
$this
->
checkString
(
'gsub'
,
$s
);
$this
->
checkTypeOptional
(
'gsub'
,
4
,
$n
,
'number'
,
null
);
if
(
$n
===
null
)
{
$n
=
-
1
;
}
elseif
(
$n
<
1
)
{
return
[
$s
,
0
];
}
[
$re
,
$capt
,
$anypos
]
=
$this
->
patternToRegex
(
$pattern
,
'^'
,
'gsub'
);
$captures
=
[];
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
];
$c
=
[
$m
[
0
][
0
]
];
foreach
(
$this
->
addCapturesFromMatch
(
[],
$s
,
$m
,
$capt
,
false
)
as
$k
=>
$v
)
{
$k
++;
$c
[
"m$k"
]
=
$v
;
}
$captures
[]
=
$c
;
if
(
$n
>=
0
&&
count
(
$captures
)
>=
$n
)
{
break
;
}
}
}
switch
(
$this
->
getLuaType
(
$repl
)
)
{
case
'string'
:
case
'number'
:
$cb
=
static
function
(
$m
)
use
(
$repl
,
$anypos
,
&
$captures
)
{
if
(
$anypos
)
{
$m
=
array_shift
(
$captures
);
}
return
preg_replace_callback
(
'/%([%0-9])/'
,
static
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"
];
}
elseif
(
$x
===
'1'
)
{
// Match undocumented Lua string.gsub behavior
return
$m
[
0
];
}
else
{
throw
new
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
);
}
$x
=
$m
[
'm1'
]
??
$m
[
0
];
if
(
!
isset
(
$repl
[
$x
]
)
)
{
return
$m
[
0
];
}
$type
=
$this
->
getLuaType
(
$repl
[
$x
]
);
if
(
$type
!==
'string'
&&
$type
!==
'number'
)
{
throw
new
LuaError
(
"invalid replacement value (a $type)"
);
}
return
$repl
[
$x
];
};
break
;
case
'function'
:
$interpreter
=
$this
->
getInterpreter
();
$cb
=
function
(
$m
)
use
(
$interpreter
,
$capt
,
$repl
,
$anypos
,
&
$captures
)
{
if
(
$anypos
)
{
$m
=
array_shift
(
$captures
);
}
$args
=
[];
if
(
count
(
$capt
)
)
{
foreach
(
$capt
as
$i
=>
$pos
)
{
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
$args
[]
=
$m
[
"m$i"
];
}
}
else
{
$args
[]
=
$m
[
0
];
}
$ret
=
$interpreter
->
callFunction
(
$repl
,
...
$args
);
if
(
count
(
$ret
)
===
0
||
$ret
[
0
]
===
null
)
{
return
$m
[
0
];
}
$type
=
$this
->
getLuaType
(
$ret
[
0
]
);
if
(
$type
!==
'string'
&&
$type
!==
'number'
)
{
throw
new
LuaError
(
"invalid replacement value (a $type)"
);
}
return
$ret
[
0
];
};
break
;
default
:
$this
->
checkType
(
'gsub'
,
3
,
$repl
,
'function or table or string'
);
throw
new
LogicException
(
'checkType above should have failed'
);
}
$skippedMatches
=
0
;
$count
=
0
;
$s2
=
preg_replace_callback
(
$re
,
$cb
,
$s
,
$n
,
$count
);
if
(
$s2
===
null
)
{
$this
->
handlePCREError
(
preg_last_error
(),
$pattern
);
}
return
[
$s2
,
$count
-
$skippedMatches
];
}
/**
* Handle a PCRE error
* @param int $error From preg_last_error()
* @param string $pattern Pattern being matched
* @throws 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
LuaError
(
"PCRE internal error"
);
case
PREG_BACKTRACK_LIMIT_ERROR
:
throw
new
LuaError
(
"PCRE backtrack limit reached while matching pattern '$pattern'"
);
case
PREG_RECURSION_LIMIT_ERROR
:
throw
new
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
LuaError
(
"PCRE bad UTF-8 error"
);
case
PREG_BAD_UTF8_OFFSET_ERROR
:
// Shouldn't happen, but just in case
throw
new
LuaError
(
"PCRE bad UTF-8 offset error"
);
case
$PREG_JIT_STACKLIMIT_ERROR
:
throw
new
LuaError
(
"PCRE JIT stack limit reached while matching pattern '$pattern'"
);
default
:
throw
new
LuaError
(
"PCRE error code $error while matching pattern '$pattern'"
);
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Wed, Aug 19, 00:10 (2 d, 19 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
fd/7e/cbe12e0d888917851ef6990e97dc
Default Alt Text
UstringLibrary.php (24 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment