data = $data; } /** * The value, or null with `$ok` false when it is not something this * reader accepts. * * @param string $data Serialised text. * @param bool $ok Set to whether it was read. * @return mixed */ public static function parse( string $data, ?bool &$ok = null ) { $reader = new self( $data ); try { $value = $reader->value( 0 ); $ok = strlen( $data ) === $reader->pos; return $ok ? $value : null; } catch ( UnexpectedValueException $e ) { $ok = false; return null; } } /** * @param int $depth Current nesting. * @return mixed */ private function value( int $depth ) { if ( $depth > self::MAX_DEPTH ) { throw new UnexpectedValueException( 'too deep' ); } $type = $this->data[ $this->pos ] ?? ''; switch ( $type ) { case 'N': $this->expect( 'N;' ); return null; case 'b': $this->expect( 'b:' ); $bool = $this->until( ';' ); if ( '0' !== $bool && '1' !== $bool ) { throw new UnexpectedValueException( 'bool' ); } return '1' === $bool; case 'i': $this->expect( 'i:' ); $int = $this->until( ';' ); if ( 1 !== preg_match( '/^-?[0-9]+$/D', $int ) ) { throw new UnexpectedValueException( 'int' ); } return (int) $int; case 'd': $this->expect( 'd:' ); $float = $this->until( ';' ); if ( ! is_numeric( $float ) && ! in_array( $float, array( 'INF', '-INF', 'NAN' ), true ) ) { throw new UnexpectedValueException( 'float' ); } return (float) $float; case 's': $this->expect( 's:' ); $string = $this->string(); $this->expect( ';' ); return $string; case 'a': $this->expect( 'a:' ); return $this->members( $depth, '}' ); case 'O': $this->expect( 'O:' ); if ( 'stdClass' !== $this->string() ) { throw new UnexpectedValueException( 'class' ); } $this->expect( ':' ); return $this->members( $depth, '}' ); } throw new UnexpectedValueException( 'type' ); } /** * `:{...}` of an array or stdClass. * * @param int $depth Current nesting. * @param string $close Closing character. * @return array */ private function members( int $depth, string $close ): array { $count = $this->until( ':' ); if ( 1 !== preg_match( '/^[0-9]{1,7}$/D', $count ) ) { throw new UnexpectedValueException( 'count' ); } $this->expect( '{' ); $members = array(); for ( $i = 0; $i < (int) $count; $i++ ) { $key = $this->value( self::MAX_DEPTH ); if ( ! is_int( $key ) && ! is_string( $key ) ) { throw new UnexpectedValueException( 'key' ); } $members[ $key ] = $this->value( $depth + 1 ); } $this->expect( $close ); return $members; } /** * `:""`. */ private function string(): string { $length = $this->until( ':' ); if ( 1 !== preg_match( '/^[0-9]{1,9}$/D', $length ) ) { throw new UnexpectedValueException( 'length' ); } $this->expect( '"' ); if ( $this->pos + (int) $length > strlen( $this->data ) ) { throw new UnexpectedValueException( 'short' ); } $string = substr( $this->data, $this->pos, (int) $length ); $this->pos += (int) $length; $this->expect( '"' ); return $string; } /** * The text up to (not including) $char, consuming $char. * * @param string $char One character. */ private function until( string $char ): string { $end = strpos( $this->data, $char, $this->pos ); if ( false === $end ) { throw new UnexpectedValueException( 'unterminated' ); } $text = substr( $this->data, $this->pos, $end - $this->pos ); $this->pos = $end + 1; return $text; } /** * @param string $text Literal expected next. */ private function expect( string $text ): void { if ( substr( $this->data, $this->pos, strlen( $text ) ) !== $text ) { throw new UnexpectedValueException( 'unexpected text' ); } $this->pos += strlen( $text ); } }