From c9b93d12b9d5cb952ac1686651ba75682235fd40 Mon Sep 17 00:00:00 2001 From: Ronan Lenouvel Date: Thu, 16 Jul 2026 22:17:42 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=A7=20fix(TiffReader):=20ne=20pas?= =?UTF-8?q?=20rejeter=20un=20CR2=20dont=20le=20MakerNote=20d=C3=A9passe=20?= =?UTF-8?q?64=20Ko?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un Canon EOS 5D (2005) levait CorruptedFileException sur un fichier parfaitement valide : Entrée 0x927C : 75080 octets de données annoncés, taille absurde. 0x927C est MakerNote, le bloc de métadonnées propriétaires du constructeur. 75 Ko y est parfaitement ordinaire. MAX_VALUE_LENGTH, garde-fou contre les fichiers hostiles, rejetait un fichier légitime — un faux positif de sécurité. Le plafond avait deux torts. Il était arbitraire : la vraie borne est la taille du fichier, une valeur ne pouvant pas déborder de ce qui la contient. Et il levait au lieu d'ignorer : aucun des neuf tags exploités par ce package n'approche 64 Ko — ce sont des offsets, des tailles, des dimensions, un nom de fabricant. MakerNote est traversé, jamais lu. Désormais : une valeur plus grande que le fichier lève (la structure ment) ; une valeur simplement trop grosse pour nous intéresser n'est pas résolue, et l'entrée reste traversable. Lire 75 Ko pour les jeter était du gaspillage à chaque appel. Vérifié sur quatre générations de 5D — 2005, 2008 (dont sRAW1) et 2016 — soit onze ans d'écart et du 12 au 30 Mpx. C'est le risque §12.1 du plan, traité. --- src/Parser/Tiff/TiffReader.php | 29 +++++++++++++--- tests/Unit/Parser/Tiff/TiffReaderTest.php | 40 +++++++++++++++++++++-- 2 files changed, 62 insertions(+), 7 deletions(-) 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); From d206454efe51fee0e1b53eb36b340a857464c73a Mon Sep 17 00:00:00 2001 From: Ronan Lenouvel Date: Thu, 16 Jul 2026 22:18:55 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=96=20docs(readme):=20neuf=20appar?= =?UTF-8?q?eils=20valid=C3=A9s,=20dont=20quatre=20g=C3=A9n=C3=A9rations=20?= =?UTF-8?q?de=205D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le Canon 5D de 2005 rejoint la liste après le fix du MakerNote. Quatre générations de la même lignée sont désormais couvertes — 2005, 2008 (dont sRAW1) et 2016, du 12 au 30 Mpx. C'est délibéré : la structure d'un RAW dérive entre générations, et c'est exactement là que les parseurs de format cassent. Le README le dit. --- README.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) 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