fix: truncate URI Template prefix modifier by character, not byte (#192)
Operator::inject() applied the ":N" prefix modifier with the byte-oriented substr(), splitting multibyte UTF-8 characters in half and producing invalid percent-encoded output (e.g. the euro sign truncated to its first byte only). RFC 6570 Section 2.4.1 defines the prefix length as a count of characters, so use mb_substr() with an explicit UTF-8 encoding instead. ext-mbstring is already a hard dependency of this package.
This commit is contained in:
1 parent
0a2c361a43
commit
98e2fac9c1
2 files changed
+18
-2
No files matched your search
@@ -19,10 +19,10 @@ use Stringable;
|
|||||||
|
|
||||||
use function implode;
|
use function implode;
|
||||||
use function is_array;
|
use function is_array;
|
||||||
|
use function mb_substr;
|
||||||
use function preg_match;
|
use function preg_match;
|
||||||
use function rawurlencode;
|
use function rawurlencode;
|
||||||
use function str_contains;
|
use function str_contains;
|
||||||
use function substr;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processing behavior according to the expression type operator.
|
* Processing behavior according to the expression type operator.
|
||||||
@@ -156,7 +156,9 @@ enum Operator: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (':' === $varSpec->modifier) {
|
if (':' === $varSpec->modifier) {
|
||||||
$value = substr($value, 0, $varSpec->position);
|
// RFC 6570 section 2.4.1: the prefix length counts characters, not octets,
|
||||||
|
// so truncating has to be multibyte aware.
|
||||||
|
$value = mb_substr($value, 0, $varSpec->position, 'UTF-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
return [$this->decode($value), $this->isNamed()];
|
return [$this->decode($value), $this->isNamed()];
|
||||||
|
|||||||
@@ -261,6 +261,20 @@ final class UriTemplateTest extends TestCase
|
|||||||
self::assertSame($expectedUri, $uriTemplate->expandToUrlOrFail($variables)->toAsciiString());
|
self::assertSame($expectedUri, $uriTemplate->expandToUrlOrFail($variables)->toAsciiString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPrefixModifierTruncatesByCharacterNotByte(): void
|
||||||
|
{
|
||||||
|
// RFC 6570 section 2.4.1: the prefix counts characters, so a multibyte
|
||||||
|
// character must never be split. The euro sign and the G-clef are 3 and 4
|
||||||
|
// bytes but one character each.
|
||||||
|
$uriTemplate = new UriTemplate('{?currency:1}{clef:1}');
|
||||||
|
$variables = [
|
||||||
|
'currency' => "\u{20AC}uro",
|
||||||
|
'clef' => "\u{1D11E}stave",
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertSame('?currency=%E2%82%AC%F0%9D%84%9E', $uriTemplate->expand($variables)->toString());
|
||||||
|
}
|
||||||
|
|
||||||
public function testDisallowNestedArrayExpansion(): void
|
public function testDisallowNestedArrayExpansion(): void
|
||||||
{
|
{
|
||||||
$template = 'http://example.com{?query,data*,foo*}';
|
$template = 'http://example.com{?query,data*,foo*}';
|
||||||
|
|||||||
Reference in new issue
Block a user