diff --git a/README.md b/README.md index 1a2c641..b69482e 100644 --- a/README.md +++ b/README.md @@ -125,14 +125,21 @@ Verified against real files from [raw.pixls.us](https://raw.pixls.us/) (CC0): All five formats are verified against real camera files: -| Camera | Format | File | Preview extracted | -|--------------------|--------|-------|-----------------------| -| Canon EOS 5D Mk IV | CR2 | 62 MB | 6720×4480 — 2047 KB | -| Canon EOS R | CR3 | 30 MB | 1620×1080 — 228 KB | -| Canon EOS RP | CR3 | 7 MB | 1620×1080 — 328 KB | -| Nikon D750 | NEF | 25 MB | 6016×4016 — 952 KB | -| Sony α7 (ILCE-7) | ARW | 24 MB | 1616×1080 — 460 KB | -| Apple iPhone 12 Pro| DNG | 29 MB | 4032×3024 — 5239 KB | +| Camera | Year | Format | File | Preview extracted | +|------------------------|------|--------|-------|---------------------| +| Canon EOS 5D | 2005 | CR2 | 13 MB | 2496×1664 — 1656 KB | +| Canon EOS 5D Mark II | 2008 | CR2 | 27 MB | 5616×3744 — 1980 KB | +| Canon 5D Mark II sRAW1 | 2008 | CR2 | 15 MB | 5616×3744 — 1973 KB | +| Canon EOS 5D Mark IV | 2016 | CR2 | 62 MB | 6720×4480 — 2047 KB | +| Canon EOS R | 2018 | CR3 | 30 MB | 1620×1080 — 228 KB | +| Canon EOS RP | 2019 | CR3 | 7 MB | 1620×1080 — 328 KB | +| Nikon D750 | 2014 | NEF | 25 MB | 6016×4016 — 952 KB | +| Sony α7 (ILCE-7) | 2013 | ARW | 24 MB | 1616×1080 — 460 KB | +| Apple iPhone 12 Pro | 2020 | DNG | 29 MB | 4032×3024 — 5239 KB | + +Four generations of the same Canon 5D line — 2005 to 2016, 12 to 30 megapixels — are +covered deliberately: RAW layout drifts between generations, and that drift is where +format parsers break. Every preview above is checked with `imagecreatefromstring()` — not just decoded headers, but actually opened. Extraction takes **1–9 ms** on files up to 62 MB: the file is never diff --git a/src/Parser/Tiff/TiffReader.php b/src/Parser/Tiff/TiffReader.php index 0705324..567e473 100644 --- a/src/Parser/Tiff/TiffReader.php +++ b/src/Parser/Tiff/TiffReader.php @@ -29,8 +29,18 @@ final class TiffReader /** Garde-fou contre une chaîne d'IFD artificiellement longue. */ private const MAX_IFD_CHAIN = 64; - /** Aucune valeur de tag exploitée ici n'approche cette taille. */ - private const MAX_VALUE_LENGTH = 65536; + /** + * Au-delà, une valeur n'est pas résolue — l'entrée reste lisible. + * + * Aucun tag exploité par ce package n'approche cette taille : ce sont des + * offsets, des tailles, des dimensions, un nom de fabricant. Les gros blocs + * sont des métadonnées propriétaires (`MakerNote` pèse couramment 75 Ko dans + * un CR2) que l'on traverse sans jamais les lire. + * + * Ne pas les résoudre évite deux choses : gaspiller la lecture, et rejeter + * un fichier parfaitement valide — ce qui arrivait au Canon 5D de 2005. + */ + private const MAX_RESOLVED_VALUE_LENGTH = 65536; /** Taille en octets de chaque type TIFF 6.0, indexée par code de type. */ private const TYPE_SIZES = [ @@ -183,14 +193,25 @@ private function readEntry(string $bytes): IfdEntry $length = $size * $count; - if ($length > self::MAX_VALUE_LENGTH) { + // Une valeur ne peut pas être plus grande que le fichier qui la porte : + // c'est une taille absurde, donc une structure qui ment. + if ($length > $this->fileSize) { throw new CorruptedFileException(sprintf( - 'Entrée 0x%04X : %d octets de données annoncés, taille absurde.', + 'Entrée 0x%04X : %d octets annoncés, plus que le fichier entier (%d).', $tag, $length, + $this->fileSize, )); } + // Trop gros pour être un tag que ce package exploite : on garde l'entrée + // — elle reste traversable — mais on ne lit pas sa valeur. Un MakerNote + // de 75 Ko n'est pas une corruption, c'est une métadonnée qui ne nous + // regarde pas. + if ($length > self::MAX_RESOLVED_VALUE_LENGTH) { + return new IfdEntry($tag, $type, $count); + } + // Règle des 4 octets : au-delà, le champ porte un offset absolu et non // la valeur elle-même. S'y tromper donne des offsets qui ressemblent à // des données valides. diff --git a/tests/Unit/Parser/Tiff/TiffReaderTest.php b/tests/Unit/Parser/Tiff/TiffReaderTest.php index 853e6fa..4788d52 100644 --- a/tests/Unit/Parser/Tiff/TiffReaderTest.php +++ b/tests/Unit/Parser/Tiff/TiffReaderTest.php @@ -206,18 +206,52 @@ public function testStopsFollowingChainAfterTooManyIfds(): void self::assertCount(64, $this->reader($bytes)->readIfdOffsets()); } - public function testThrowsWhenValueLengthIsAbsurd(): void + public function testThrowsWhenValueLengthExceedsFileSize(): void { $this->expectException(CorruptedFileException::class); - $this->expectExceptionMessage('absurde'); // count = 100 000 × 4 octets (LONG) = 400 Ko annoncés dans un fichier - // de 26 octets : refuser avant d'allouer. + // de 26 octets : une valeur ne peut pas déborder du fichier qui la porte. $this->reader($this->tiff('II', [ [TiffTag::SubIfds->value, 4, 100000, pack('V', 26)], ]))->readIfd(8); } + public function testDoesNotResolveOversizedValuesButKeepsTheEntry(): void + { + // Le MakerNote d'un CR2 pèse couramment plus de 64 Ko. Le package ne + // l'exploite pas : le lire serait du gaspillage, et le rejeter ferait + // échouer un fichier parfaitement valide — c'est ce qui arrivait au + // Canon 5D de 2005. + $big = str_repeat("\xAB", 80000); + $offset = 8 + 2 + 12 + 4; + + $bytes = $this->tiff('II', [ + [0x927C, 7, strlen($big), pack('V', $offset)], // MakerNote, UNDEFINED + ]) . $big; + + $entries = $this->reader($bytes)->readIfd(8); + + // L'entrée existe et reste traversable ; seule sa valeur n'est pas résolue. + self::assertCount(1, $entries); + self::assertSame(0x927C, $entries[0]->tag); + self::assertSame(80000, $entries[0]->count); + self::assertSame([], $entries[0]->values); + } + + public function testStillResolvesLargeValuesOfExploitedTags(): void + { + // Contre-épreuve : un tag utile reste résolu même avec plusieurs valeurs. + $payload = pack('V', 111) . pack('V', 222) . pack('V', 333); + $offset = 8 + 2 + 12 + 4; + + $bytes = $this->tiff('II', [ + [TiffTag::SubIfds->value, 4, 3, pack('V', $offset)], + ]) . $payload; + + self::assertSame([111, 222, 333], $this->reader($bytes)->readIfd(8)[0]->values); + } + public function testThrowsWhenIfdOffsetIsOutOfBounds(): void { $this->expectException(CorruptedFileException::class);