webmozarts / assert

Assertions to validate method input/output with nice error messages.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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);
    }