Skip to content
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
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 25 additions & 4 deletions src/Parser/Tiff/TiffReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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.
Expand Down
40 changes: 37 additions & 3 deletions tests/Unit/Parser/Tiff/TiffReaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down