Skip to content

Cleanup #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Bytes.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace gapple\StructuredFields;

class Bytes
class Bytes implements \Stringable
{
public function __construct(private readonly string $value)
{
Expand Down
3 changes: 0 additions & 3 deletions src/Dictionary.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public static function fromArray(array $array): self
}

/**
* @param string $name
* @return TupleInterface|array{mixed, object}|null
*/
public function __get(string $name): mixed
Expand All @@ -46,9 +45,7 @@ public function __get(string $name): mixed
}

/**
* @param string $name
* @param TupleInterface|array{mixed, object} $value
* @return void
*/
public function __set(string $name, mixed $value): void
{
Expand Down
2 changes: 1 addition & 1 deletion src/DisplayString.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace gapple\StructuredFields;

class DisplayString
class DisplayString implements \Stringable
{
public function __construct(private readonly string $value)
{
Expand Down
5 changes: 1 addition & 4 deletions src/InnerList.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class InnerList implements TupleInterface

/**
* @param array<TupleInterface|array{mixed, object}> $value
* @param object|null $parameters
*/
public function __construct(array $value, ?object $parameters = null)
{
Expand All @@ -25,11 +24,10 @@ public function __construct(array $value, ?object $parameters = null)
*
* @param array<mixed> $array
* An array of bare items or TupleInterface objects.
* @return InnerList
*/
public static function fromArray(array $array): InnerList
{
array_walk($array, function (&$item) {
array_walk($array, function (&$item): void {
if (!$item instanceof TupleInterface) {
$item = new Item($item);
} elseif ($item instanceof InnerList) {
Expand All @@ -43,7 +41,6 @@ public static function fromArray(array $array): InnerList

/**
* @param TupleInterface|array{mixed, object} $value
* @return void
*/
private static function validateItemType(mixed $value): void
{
Expand Down
4 changes: 0 additions & 4 deletions src/Item.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ class Item implements TupleInterface
{
use TupleTrait;

/**
* @param mixed $value
* @param object|null $parameters
*/
public function __construct(mixed $value, ?object $parameters = null)
{
$this->value = $value;
Expand Down
8 changes: 1 addition & 7 deletions src/OuterList.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,10 @@ public function __construct(array $value = [])
* Create an OuterList from an array of bare values.
*
* @param array<mixed> $array
* @return OuterList
*/
public static function fromArray(array $array): OuterList
{
array_walk($array, function (&$item) {
array_walk($array, function (&$item): void {
if (!$item instanceof TupleInterface) {
if (is_array($item)) {
$item = InnerList::fromArray($item);
Expand All @@ -51,7 +50,6 @@ public static function fromArray(array $array): OuterList

/**
* @param TupleInterface|array{mixed, object} $value
* @return void
*/
private static function validateItemType(mixed $value): void
{
Expand All @@ -77,7 +75,6 @@ public function getIterator(): \Iterator

/**
* @param int $offset
* @return bool
*/
public function offsetExists($offset): bool
{
Expand All @@ -86,7 +83,6 @@ public function offsetExists($offset): bool

/**
* @param int $offset
* @return mixed
* @phpstan-return TupleInterface|array{mixed, object}|null
*/
public function offsetGet($offset): mixed
Expand All @@ -97,7 +93,6 @@ public function offsetGet($offset): mixed
/**
* @param int|null $offset
* @param TupleInterface|array{mixed, object} $value
* @return void
*/
public function offsetSet($offset, $value): void
{
Expand All @@ -112,7 +107,6 @@ public function offsetSet($offset, $value): void

/**
* @param int $offset
* @return void
*/
public function offsetUnset($offset): void
{
Expand Down
9 changes: 0 additions & 9 deletions src/Parameters.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,11 @@ public static function fromArray(array $array): self
return $parameters;
}

/**
* @param string $name
* @return mixed|null
*/
public function __get(string $name): mixed
{
return $this->value[$name] ?? null;
}

/**
* @param string $name
* @param mixed $value
* @return void
*/
public function __set(string $name, mixed $value): void
{
$this->value[$name] = $value;
Expand Down
47 changes: 24 additions & 23 deletions src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ public static function parseDictionary(string $string): Dictionary
while (true) {
$key = self::parseKey($input);

if ($input->isChar('=')) {
$input->consumeChar();
if ($input->skipNextCharIf('=')) {
$value->{$key} = self::parseItemOrInnerList($input);
} else {
// Bare boolean true value.
Expand Down Expand Up @@ -85,7 +84,7 @@ public static function parseList(string $string): OuterList

private static function parseItemOrInnerList(ParsingInput $input): TupleInterface
{
if ($input->isChar('(')) {
if ($input->isNextChar('(')) {
return self::parseInnerList($input);
} else {
return self::doParseItem($input);
Expand All @@ -104,8 +103,7 @@ private static function parseInnerList(ParsingInput $input): InnerList
while (!$input->empty()) {
$input->trim();

if ($input->isChar(')')) {
$input->consumeChar();
if ($input->skipNextCharIf(')')) {
return new InnerList(
$value,
self::parseParameters($input)
Expand All @@ -114,7 +112,7 @@ private static function parseInnerList(ParsingInput $input): InnerList

$value[] = self::doParseItem($input);

if (!($input->isChar(' ') || $input->isChar(')'))) {
if (!($input->isNextChar(' ') || $input->isNextChar(')'))) {
if ($input->empty()) {
break;
}
Expand All @@ -126,8 +124,6 @@ private static function parseInnerList(ParsingInput $input): InnerList
}

/**
* @param string $string
*
* @return Item
* A [value, parameters] tuple.
*/
Expand Down Expand Up @@ -190,16 +186,15 @@ private static function parseBareItem(ParsingInput $input): mixed
private static function parseParameters(ParsingInput $input): Parameters
{
$parameters = new Parameters();
while ($input->isChar(';')) {
$input->consumeChar();
while ($input->skipNextCharIf(';')) {
$input->trim();

$key = self::parseKey($input);
$parameters->{$key} = true;

if ($input->isChar('=')) {
$input->consumeChar();
if ($input->skipNextCharIf('=')) {
$parameters->{$key} = self::parseBareItem($input);
} else {
$parameters->{$key} = true;
}
}

Expand Down Expand Up @@ -275,7 +270,7 @@ private static function parseString(ParsingInput $input): string
}
} elseif ($char === '"') {
return $output;
} elseif (ord($char) <= 0x1f || ord($char) >= 0x7f) {
} elseif (!ctype_print($char)) {
throw new ParseException('Invalid character in string at position ' . ($input->position() - 1));
}

Expand All @@ -301,18 +296,21 @@ private static function parseDisplayString(ParsingInput $string): DisplayString
while (!$string->empty()) {
$char = $string->consumeChar();

if (ord($char) <= 0x1f || ord($char) >= 0x7f) {
if (!ctype_print($char)) {
throw new ParseException(
'Invalid character in display string at position ' . ($string->position() - 1)
);
} elseif ($char === '%') {
try {
$encodedString .= '%' . $string->consumeRegex('/^[0-9a-f]{2}/');
} catch (\RuntimeException) {
if ($string->remainingLength() < 2) {
break;
}
$encodedChar = $string->consume(2);
if (!ctype_xdigit($encodedChar) || ctype_upper($encodedChar)) {
throw new ParseException(
'Invalid hex values in display string at position ' . ($string->position() - 1)
);
}
$encodedString .= '%' . $encodedChar;
} elseif ($char === '"') {
$displayString = new DisplayString(rawurldecode($encodedString));
// An invalid UTF-8 subject will cause the preg_* function to match nothing.
Expand All @@ -334,16 +332,19 @@ private static function parseDisplayString(ParsingInput $string): DisplayString
*/
private static function parseToken(ParsingInput $input): Token
{
// Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing
// 3.2.6. Field Value Components
// @see https://tools.ietf.org/html/rfc7230#section-3.2.6
$tchar = preg_quote("!#$%&'*+-.^_`|~");
// RFC 9110: HTTP Semantics (5.6.2. Tokens)
// @see https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens
// $tchar = preg_quote("!#$%&'*+-.^_`|~");
$tchar = "!#$%&'*+\-.^_`|~";

// parseToken is only called by parseBareItem if the initial character
// is valid, so a Token object is always returned. If there is an
// invalid character in the token, the public function that was called
// will detect that the remainder of the input string is invalid.
return new Token($input->consumeRegex('/^([a-z*][a-z0-9:\/' . $tchar . ']*)/i'));
return new Token($input->consumeRegex('/^(
(?:\*|[a-z]) # an alphabetic character or "*"
[a-z0-9:\/' . $tchar . ']* # zero to many token characters
)/ix'));
}

/**
Expand Down
25 changes: 24 additions & 1 deletion src/ParsingInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@ public function remaining(): string
return substr($this->value, $this->position);
}

public function remainingLength(): int
{
return $this->length - $this->position();
}

/**
* Trim whitespace from beginning of string.
*
* @param bool $ows
* Whether all Optional Whitespace characters should be trimmed. If false, only space characters are trimmed.
* @see https://tools.ietf.org/html/rfc7230#section-3.2.3
* @return void
*/
public function trim(bool $ows = false): void
{
Expand All @@ -54,7 +58,16 @@ public function trim(bool $ows = false): void
}
}

/**
* @deprecated in 2.3.0 and will be removed in 3.0.0
* @codeCoverageIgnore
*/
public function isChar(string $char): bool
{
return $this->isNextChar($char);
}

public function isNextChar(string $char): bool
{
assert(strlen($char) === 1);

Expand All @@ -70,6 +83,15 @@ public function getChar(): string
return $this->value[$this->position];
}

public function skipNextCharIf(string $char): bool
{
if ($this->isNextChar($char)) {
$this->position++;
return true;
}
return false;
}

/**
* @phpstan-impure
*/
Expand Down Expand Up @@ -114,6 +136,7 @@ public function consumeString(string $value): void
public function consumeRegex(string $pattern): string
{
assert(str_starts_with($pattern, '/^'));
assert(!preg_match('/\$\/[a-z]+$/i', $pattern));

if (preg_match($pattern, $this->remaining(), $matches)) {
$this->position += strlen($matches[0]);
Expand Down
30 changes: 15 additions & 15 deletions src/Serializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@ class Serializer
/**
* Serialize an item with optional parameters.
*
* @param mixed $value
* @param Item|mixed $value
* A bare value, or an Item object.
* @param object|null $parameters
* An optional object containing parameter values if a bare value is provided.
*
* @return string
* The serialized value.
* If a bare value is provided, an optional object containing parameter values.
*/
public static function serializeItem(mixed $value, ?object $parameters = null): string
{
Expand Down Expand Up @@ -43,7 +40,6 @@ public static function serializeItem(mixed $value, ?object $parameters = null):

/**
* @param iterable<TupleInterface|array{mixed, object}> $value
* @return string
*/
public static function serializeList(iterable $value): string
{
Expand Down Expand Up @@ -205,11 +201,10 @@ private static function serializeDecimal(float $value): string

private static function serializeString(string $value): string
{
if (preg_match('/[^\x20-\x7E]/i', $value)) {
if (!empty($value) && !ctype_print($value)) {
throw new SerializeException("Invalid characters in string");
}

return '"' . preg_replace('/(["\\\])/', '\\\$1', $value) . '"';
return '"' . str_replace(['\\', '"'], ['\\\\', '\"'], $value) . '"';
}

private static function serializeDisplayString(DisplayString $value): string
Expand All @@ -224,12 +219,17 @@ private static function serializeDisplayString(DisplayString $value): string

private static function serializeToken(Token $value): string
{
// Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing
// 3.2.6. Field Value Components
// @see https://tools.ietf.org/html/rfc7230#section-3.2.6
$tchar = preg_quote("!#$%&'*+-.^_`|~");

if (!preg_match('/^((?:\*|[a-z])[a-z0-9:\/' . $tchar . ']*)$/i', (string) $value)) {
// RFC 9110: HTTP Semantics (5.6.2. Tokens)
// @see https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens
// $tchar = preg_quote("!#$%&'*+-.^_`|~");
$tchar = "!#$%&'*+\-.^_`|~";

if (
!preg_match('/^(
(?:\*|[a-z]) # an alphabetic character or "*"
[a-z0-9:\/' . $tchar . ']* # zero to many token characters
)$/ix', (string) $value)
) {
throw new SerializeException('Invalid characters in token');
}

Expand Down
Loading
Loading