Multi-layer encoding
theofidry opened this issue · comments
When calling assert like so:
Assert::count(..., message: $message);
$message
is forwarded to sprintf
. As a result if you are to do something like this:
Assert::count(
...,
message: 'This is my custom message %s', // <- the placeholder will be filled up automatically, great!
);
Which does mean if you are using your own sprintf you should escape it first:
Assert::count(
...,
message: 'This is my %%w custom message %s', // <- manually escape "%w" as this would make sprintf fail (it is an invalid format specifier)
);
// Automatic escaping:
Assert::count(
...,
message: str_replace('%', '%%', 'This is my %%w custom message %s'),
);
So far so good. Now the problem: this does not work for all assertions. Indeed the example above with Assert::count()
will still result in ValueError: Unknown format specifier "w"
.
The root of the issue is that an assertion can rely on other assertions internally, e.g. ::count()
calls ::eq()
. So the custom escaped message becomes unescaped when passing it to ::eq()
. Making it work requires the user to find out how many items the string is evaluated to escape it accordingly (in this example twice).
I think this is a design flaw and the way it could be fixed is to escape the message passed within Assert
when relying on inner assertions:
// Assert.php
public static function count($array, $number, $message = '')
{
static::eq(
\count($array),
$number,
\sprintf(
self::sprintfEscape($message) ?: 'Expected an array to contain %d elements. Got: %d.',
$number,
\count($array)
)
);
}
public static function sprintfEscape(string $value): string {
return str_replace('%', '%%', $value);
}