diff --git a/CHANGELOG.md b/CHANGELOG.md index bc89c12..42414c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +* Added more checks when decoding JSON Files + ## [1.1.0] - 2022-11-04 Dropped support for PHP <8.1 diff --git a/composer.json b/composer.json index d8d0b76..0292dbb 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ ] }, "scripts-descriptions": { - "clean": "Recreates ", + "clean": "Recreates the build/cache directory", "phpstan": "Runs static analysis with PHPStan", "phpunit": "Runs tests with PHPUnit", "psalm": "Runs static analysis with Psalm", diff --git a/src/Json.php b/src/Json.php index ff38e9c..bb3e273 100644 --- a/src/Json.php +++ b/src/Json.php @@ -5,6 +5,9 @@ namespace Beste; use JsonException; +use SplFileInfo; +use SplFileObject; +use Throwable; use UnexpectedValueException; final class Json @@ -14,11 +17,11 @@ final class Json private const DECODE_DEFAULT = JSON_BIGINT_AS_STRING; /** - * @throws UnexpectedValueException + * param non-empty-string $json * - * @return mixed + * @throws UnexpectedValueException */ - public static function decode(string $json, ?bool $forceArray = null) + public static function decode(string $json, ?bool $forceArray = null): mixed { $forceArray = $forceArray ?? false; $flags = $forceArray ? JSON_OBJECT_AS_ARRAY : 0; @@ -31,25 +34,44 @@ public static function decode(string $json, ?bool $forceArray = null) } /** - * @throws UnexpectedValueException + * @param non-empty-string $path * - * @return mixed + * @throws UnexpectedValueException */ - public static function decodeFile(string $path, bool $forceArray = null) + public static function decodeFile(string $path, bool $forceArray = null): mixed { - if (!is_readable($path)) { - throw new UnexpectedValueException("The file at '$path' does not exist"); + $fileInfo = new SplFileInfo($path); + + if ($fileInfo->isLink() && $linkTarget = $fileInfo->getLinkTarget()) { + $fileInfo = new SplFileInfo($linkTarget); + } + + if (!$fileInfo->isFile()) { + throw new UnexpectedValueException("`$path` does not point to a file."); + } + + if (!$fileInfo->isReadable()) { + throw new UnexpectedValueException("`$path` is not readable."); + } + + $file = $fileInfo->openFile(); + $contents = $file->fread($file->getSize()); + + if ($contents === false) { + throw new UnexpectedValueException("Unable to read contents of `$path`"); } - return self::decode((string) file_get_contents($path), $forceArray); + if ($contents === '') { + throw new UnexpectedValueException("The file at `$path` is empty"); + } + + return self::decode($contents, $forceArray); } /** - * @param mixed $data - * * @throws UnexpectedValueException */ - public static function encode($data, ?int $options = null): string + public static function encode(mixed $data, ?int $options = null): string { $options = $options ?? 0; @@ -61,11 +83,9 @@ public static function encode($data, ?int $options = null): string } /** - * @param mixed $value - * * @throws UnexpectedValueException */ - public static function pretty($value, ?int $options = null): string + public static function pretty(mixed $value, ?int $options = null): string { $options = $options ?? 0; diff --git a/tests/DecodeJsonTest.php b/tests/DecodeJsonTest.php index ec52930..78a1886 100644 --- a/tests/DecodeJsonTest.php +++ b/tests/DecodeJsonTest.php @@ -5,6 +5,7 @@ namespace Beste\Json\Tests; use Beste\Json; +use SplFileObject; use UnexpectedValueException; use PHPUnit\Framework\TestCase; @@ -68,10 +69,33 @@ public function it_rejects_an_unreadable_file(): void public function it_rejects_a_file_with_invalid_json(): void { $path = __DIR__.'/invalid.json'; + assert(file_exists($path)); $this->expectException(UnexpectedValueException::class); - assert(file_exists($path)); + Json::decodeFile($path); + } + + /** @test */ + public function it_rejects_a_directory(): void + { + $this->expectException(UnexpectedValueException::class); + Json::decodeFile(__DIR__); + } + + /** @test */ + public function it_resolves_links(): void + { + $path = __DIR__.'/valid.json'; + $symlinkPath = __DIR__.'/'.__FUNCTION__.'.json'; + + try { + $this->assertNotFalse(symlink($path, $symlinkPath)); + $this->assertTrue(is_link($symlinkPath)); + + $this->assertIsObject(Json::decodeFile($symlinkPath)); + } finally { + unlink($symlinkPath); + } - $this->assertIsObject(Json::decodeFile($path)); } }