From d12645d06831d653bedf0b3a7897041168bd9448 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?=
<323649642+oc-tmueller@users.noreply.github.com>
Date: Mon, 14 Sep 2026 12:39:16 +0200
Subject: [PATCH 1/3] fix: replace Imagick coder pinning with a magic-byte
pre-check (OC10-164)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
#41832's path-based coder pin (write to a temp file, read via a "FORMAT:path"
prefix) writes every preview candidate's full content to local disk before
decoding, on every preview generation - unacceptable I/O overhead. There's
no guarantee the temp directory is tmpfs/RAM-backed (TempManager falls back
through system config, ini, env vars, sys_get_temp_dir(), none of which
forces tmpfs), and no in-memory-only Imagick pattern exists anywhere in this
codebase to avoid it another way.
Replace coder pinning with a fast, zero-I/O, pure-PHP magic-byte check per
provider, run against the raw content before Imagick is ever invoked. If the
leading bytes don't match what this provider's format requires, throw
before Imagick sees the content - same as the existing isDangerousToDecode()
throw path, which is unchanged. If they do match, decode with the original,
simple, unpinned readImageBlob() - no temp file, no setFormat(), no coder
pin of any kind. This works because Imagick's own auto-sniffing keys off the
exact same leading bytes the new check verifies: passing the check
guarantees Imagick will independently reach the same conclusion, so there is
no way to satisfy it with bytes that then decode via a different coder.
This also reverts the decode call to code already proven correct
pre-#41832, rather than working around the rasterization bug that mechanism
had.
getImagickFormat(string $mimeType): string becomes
hasExpectedMagicBytes(string $content): bool on every Bitmap subclass - no
mime-type parameter, since decoding is unpinned again and the check only
needs to answer "do these bytes plausibly belong to any format this
provider covers", not pick one specific coder to force. Font and Heic no
longer need to branch on mime type at all (that branching existed only to
choose which single coder to pin for PFB-vs-TTF or HEIC-vs-HEIF).
SVG.php reverts fully to its pre-#41832 simple readImageBlob() call, with no
byte-check added: the content it decodes is DOMSanitizer's serialized DOM
output, not raw uploaded bytes - sanitizeSVGContent() already returns null
(→ false, Imagick untouched) for anything that didn't parse as well-formed
XML/SVG. A DOM serializer cannot emit PostScript/PDF/binary bytes as
document-leading output, so the cross-coder-confusion risk this whole
change addresses cannot occur on that path. Office.php is untouched - its
'PDF:' prefix adds no new disk I/O, it only prefixes a path LibreOffice
itself already wrote, and is exactly the "FORMAT:path" pattern already
confirmed to decode correctly.
Rename CoderPinningTest -> MagicByteGateTest (the mechanism under test is
no longer coder pinning). Folds the separate Font "false-or-safe-blank-image"
leniency test into the main foreign-content rejection set, since Font now
also rejects deterministically before Imagick/FreeType ever runs. Adds
stronger cross-format negative cases (a genuine fixture of one format fed to
a provider for a different one - proves the gate itself, not Imagick's own
leniency on garbage bytes, is what stops decoding) and direct
ReflectionMethod boundary tests against hasExpectedMagicBytes() covering
every accepted/rejected signature per provider, independent of any Imagick
delegate being installed.
Verified in a fresh owncloudci/php:8.3 container (not a reused one - a
stale relaxed ImageMagick policy in an earlier container silently masked an
unrelated, pre-existing SVGTest/MVG-policy interaction during earlier
testing in this investigation): full tests/lib/Preview/ suite green (105
tests, 257 assertions, 9 skips - one CI-environment HEIF-coder gap already
known from #41832, the rest unrelated missing Movie/Office providers).
Confirmed the one SVGTest failure that does appear under this container's
stricter-than-production default ImageMagick policy (denies MVG, which some
SVG rendering paths need internally) is unrelated to this change - it
reproduces identically against the pre-#41827 original SVG.php code with a
bare, unhardened Imagick object, so it predates all of OC10-164's work.
Not yet verified: the magic-byte patterns against ImageMagick's own
magic.c table directly (no local ImageMagick source tree, no network
access) - any mismatch there can only cause a false rejection (a legitimate
variant Imagick would have decoded fine loses its preview), never a false
acceptance, since decoding stays unpinned and keys off the same bytes. No
.pfb fixture exists anywhere in this repo; Font's PFB check is implemented
from the documented Adobe Type 1 Font Format spec only.
Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
---
lib/private/Preview/Bitmap.php | 56 +++----
lib/private/Preview/Font.php | 15 +-
lib/private/Preview/Heic.php | 11 +-
lib/private/Preview/Illustrator.php | 7 +-
lib/private/Preview/PDF.php | 4 +-
lib/private/Preview/Photoshop.php | 4 +-
lib/private/Preview/Postscript.php | 4 +-
lib/private/Preview/SGI.php | 4 +-
lib/private/Preview/SVG.php | 19 +--
lib/private/Preview/TIFF.php | 4 +-
tests/lib/Preview/CoderPinningTest.php | 133 ----------------
tests/lib/Preview/MagicByteGateTest.php | 194 ++++++++++++++++++++++++
12 files changed, 261 insertions(+), 194 deletions(-)
delete mode 100644 tests/lib/Preview/CoderPinningTest.php
create mode 100644 tests/lib/Preview/MagicByteGateTest.php
diff --git a/lib/private/Preview/Bitmap.php b/lib/private/Preview/Bitmap.php
index ad6f11f97d5..37f87e81024 100644
--- a/lib/private/Preview/Bitmap.php
+++ b/lib/private/Preview/Bitmap.php
@@ -49,7 +49,7 @@ public function getThumbnail(File $file, $maxX, $maxY, $scalingUp) {
// Creates \Imagick object from bitmap or vector file
try {
- $bp = $this->getResizedPreview($stream, $maxX, $maxY, $file->getMimeType());
+ $bp = $this->getResizedPreview($stream, $maxX, $maxY);
} catch (\Exception $e) {
Util::writeLog('core', 'ImageMagick says: ' . $e->getmessage(), Util::ERROR);
return false;
@@ -82,40 +82,34 @@ public function isAvailable(FileInfo $file) {
* @param resource $stream the handle of the file to convert
* @param int $maxX
* @param int $maxY
- * @param string $mimeType the file's own detected mime type, used to pin the
- * Imagick coder so it can't be redirected by the file's actual content
*
* @return Imagick
*/
- private function getResizedPreview($stream, int $maxX, int $maxY, string $mimeType): Imagick {
+ private function getResizedPreview($stream, int $maxX, int $maxY): Imagick {
$content = \stream_get_contents($stream);
if ($this->isDangerousToDecode($content)) {
throw new \RuntimeException('Refusing to decode text-based content for a bitmap preview');
}
- $bp = ImagickFactory::create();
-
- # Pin the coder instead of letting Imagick's own content-sniffing pick one:
- # reading with no format set re-derives the format from a ~130-entry magic
- # table independently of isDangerousToDecode()'s check above, so content that
- # looks like PostScript/PDF (which that check must allow through for the
- # Postscript/PDF providers) would otherwise reach the Ghostscript delegate via
- # any Bitmap provider, not just those two.
- #
- # The pin has to be a "FORMAT:path" read, not setFormat()+readImageBlob(): the
- # latter does reliably reject a mismatched format, but for several coders here
- # (PDF, EPS, AI, PSD, SGI, TIFF, HEIC, HEIF - i.e. all of them) it also silently
- # skips the actual rasterization step, so setImageFormat('png') below ends up
- # with no effect and getImageBlob() returns the original, undecoded bytes.
- $tmpPath = \OC::$server->getTempManager()->getTemporaryFile();
- \file_put_contents($tmpPath, $content);
- try {
- $bp->readImage($this->getImagickFormat($mimeType) . ':' . $tmpPath);
- } finally {
- \unlink($tmpPath);
+ # Reject content whose own leading bytes don't match this provider's expected
+ # format before Imagick ever sees it, instead of pinning the coder Imagick
+ # itself uses to decode: readImageBlob() with no format set picks its coder
+ # from the same leading bytes via its own ~130-entry magic table, independently
+ # of isDangerousToDecode()'s check above - so content that looks like
+ # PostScript/PDF (which that check must allow through for the Postscript/PDF
+ # providers) would otherwise reach the Ghostscript delegate via any Bitmap
+ # provider, not just those two. Since this check and Imagick's own sniffing key
+ # off the same bytes, passing it guarantees Imagick will independently reach the
+ # same, safe conclusion - there's no way to satisfy this check with bytes that
+ # then decode via a different coder.
+ if (!$this->hasExpectedMagicBytes($content)) {
+ throw new \RuntimeException("Refusing to decode content whose signature does not match this provider's expected format");
}
+ $bp = ImagickFactory::create();
+ $bp->readImageBlob($content);
+
# setIteratorIndex(0) will make previews to be generated from the first page
$bp->setIteratorIndex(0);
@@ -144,10 +138,18 @@ private function isDangerousToDecode(string $content): bool {
}
/**
- * Maps this provider's own detected mime type(s) to the Imagick coder name that
- * must decode them - the format pinned in getResizedPreview() above.
+ * True if $content's own leading bytes plausibly belong to a format this
+ * provider is registered to decode - checked before Imagick ever sees the
+ * content, per getResizedPreview() above.
*/
- abstract protected function getImagickFormat(string $mimeType): string;
+ abstract protected function hasExpectedMagicBytes(string $content): bool;
+
+ /**
+ * True if $content has $signature's exact bytes starting at $offset.
+ */
+ protected function hasSignatureAt(string $content, string $signature, int $offset = 0): bool {
+ return \substr($content, $offset, \strlen($signature)) === $signature;
+ }
/**
* Returns a resized \Imagick object
diff --git a/lib/private/Preview/Font.php b/lib/private/Preview/Font.php
index 3cd330aa216..08b8b5ce7c3 100644
--- a/lib/private/Preview/Font.php
+++ b/lib/private/Preview/Font.php
@@ -30,12 +30,15 @@ public function getMimeType() {
return '/application\/(?:font-sfnt|x-font$)/';
}
- protected function getImagickFormat(string $mimeType): string {
- if ($mimeType === 'application/x-font') {
- return 'PFB';
+ protected function hasExpectedMagicBytes(string $content): bool {
+ foreach (["\x00\x01\x00\x00", 'OTTO', 'true', 'ttcf'] as $sfntTag) {
+ if ($this->hasSignatureAt($content, $sfntTag)) {
+ return true;
+ }
}
- # .otf and .ttf are indistinguishable by mime type alone (both application/font-sfnt);
- # TTF is what actually decodes real font files here, both tagged variants included.
- return 'TTF';
+ # PFB (Printer Font Binary): each segment starts with 0x80 followed by a
+ # segment-type byte (0x01 ASCII, 0x02 binary, 0x03 EOF).
+ return \strlen($content) >= 2 && $content[0] === "\x80"
+ && \in_array($content[1], ["\x01", "\x02", "\x03"], true);
}
}
diff --git a/lib/private/Preview/Heic.php b/lib/private/Preview/Heic.php
index e06cd65acc1..156f5c690f5 100644
--- a/lib/private/Preview/Heic.php
+++ b/lib/private/Preview/Heic.php
@@ -30,10 +30,13 @@ public function getMimeType() {
return '/image\/hei(f|c)/';
}
- protected function getImagickFormat(string $mimeType): string {
- if ($mimeType === 'image/heif') {
- return 'HEIF';
+ protected function hasExpectedMagicBytes(string $content): bool {
+ // ISO-BMFF: bytes[0:4] are a variable box size (not checked - any value is
+ // structurally valid), bytes[4:8] must be "ftyp", bytes[8:12] are the brand.
+ if (!$this->hasSignatureAt($content, 'ftyp', 4)) {
+ return false;
}
- return 'HEIC';
+ $brand = \substr($content, 8, 4);
+ return \in_array($brand, ['heic', 'heix', 'hevc', 'heim', 'heis', 'hevm', 'hevs', 'mif1', 'msf1'], true);
}
}
diff --git a/lib/private/Preview/Illustrator.php b/lib/private/Preview/Illustrator.php
index 0267b4712d1..59e9396365f 100644
--- a/lib/private/Preview/Illustrator.php
+++ b/lib/private/Preview/Illustrator.php
@@ -31,7 +31,10 @@ public function getMimeType() {
return '/application\/illustrator/';
}
- protected function getImagickFormat(string $mimeType): string {
- return 'AI';
+ protected function hasExpectedMagicBytes(string $content): bool {
+ // Modern (9+) Illustrator files are PDF-compatible ("%PDF-"); legacy pre-9
+ // files use a plain PostScript header ("%!"). Both already reach the same
+ // Ghostscript/PDF delegate as the Postscript and PDF providers.
+ return $this->hasSignatureAt($content, '%PDF-') || $this->hasSignatureAt($content, '%!');
}
}
diff --git a/lib/private/Preview/PDF.php b/lib/private/Preview/PDF.php
index 40ae6397091..c532a9d6deb 100644
--- a/lib/private/Preview/PDF.php
+++ b/lib/private/Preview/PDF.php
@@ -31,7 +31,7 @@ public function getMimeType() {
return '/application\/pdf/';
}
- protected function getImagickFormat(string $mimeType): string {
- return 'PDF';
+ protected function hasExpectedMagicBytes(string $content): bool {
+ return $this->hasSignatureAt($content, '%PDF-');
}
}
diff --git a/lib/private/Preview/Photoshop.php b/lib/private/Preview/Photoshop.php
index 7f7add29fa9..25e2c426464 100644
--- a/lib/private/Preview/Photoshop.php
+++ b/lib/private/Preview/Photoshop.php
@@ -31,7 +31,7 @@ public function getMimeType() {
return '/application\/x-photoshop/';
}
- protected function getImagickFormat(string $mimeType): string {
- return 'PSD';
+ protected function hasExpectedMagicBytes(string $content): bool {
+ return $this->hasSignatureAt($content, '8BPS');
}
}
diff --git a/lib/private/Preview/Postscript.php b/lib/private/Preview/Postscript.php
index e96c2539afa..7507ec3a307 100644
--- a/lib/private/Preview/Postscript.php
+++ b/lib/private/Preview/Postscript.php
@@ -31,7 +31,7 @@ public function getMimeType() {
return '/application\/postscript/';
}
- protected function getImagickFormat(string $mimeType): string {
- return 'EPS';
+ protected function hasExpectedMagicBytes(string $content): bool {
+ return $this->hasSignatureAt($content, '%!');
}
}
diff --git a/lib/private/Preview/SGI.php b/lib/private/Preview/SGI.php
index c9c24d4ab52..32c0cad585e 100644
--- a/lib/private/Preview/SGI.php
+++ b/lib/private/Preview/SGI.php
@@ -28,7 +28,7 @@ public function getMimeType() {
return '/image\/sgi/';
}
- protected function getImagickFormat(string $mimeType): string {
- return 'SGI';
+ protected function hasExpectedMagicBytes(string $content): bool {
+ return $this->hasSignatureAt($content, "\x01\xDA");
}
}
diff --git a/lib/private/Preview/SVG.php b/lib/private/Preview/SVG.php
index f750de84e34..972c463c100 100644
--- a/lib/private/Preview/SVG.php
+++ b/lib/private/Preview/SVG.php
@@ -58,18 +58,13 @@ public function getThumbnail(File $file, $maxX, $maxY, $scalingUp) {
return false;
}
- # Pin the coder: reading with no format set would let Imagick's own content-
- # sniffing pick the coder independently of the svg:sanitize/embed/decode
- # options above and of the isDangerousToDecode()-style reasoning in Bitmap.php.
- # This has to be a "SVG:path" read, not setFormat()+readImageBlob(): the latter
- # silently skips the actual rasterization step, same as in Bitmap.php.
- $tmpPath = \OC::$server->getTempManager()->getTemporaryFile();
- \file_put_contents($tmpPath, $output);
- try {
- $imagick->readImage('SVG:' . $tmpPath);
- } finally {
- \unlink($tmpPath);
- }
+ # $output is DOMSanitizer's serialized DOM output, not raw uploaded bytes -
+ # sanitizeSVGContent() already returned null (handled above) for anything
+ # that didn't parse as well-formed XML/SVG. A DOM serializer cannot emit
+ # PostScript/PDF/binary bytes as document-leading output, so there's no
+ # cross-coder-confusion risk here the way there is for raw file content in
+ # Bitmap.php - no need to pin the coder Imagick decodes this with.
+ $imagick->readImageBlob($output);
$imagick->setImageFormat('png32');
} catch (\Exception $e) {
\OCP\Util::writeLog('core', $e->getmessage(), \OCP\Util::ERROR);
diff --git a/lib/private/Preview/TIFF.php b/lib/private/Preview/TIFF.php
index 9f7ff4bfedc..7cd2bce359d 100644
--- a/lib/private/Preview/TIFF.php
+++ b/lib/private/Preview/TIFF.php
@@ -31,7 +31,7 @@ public function getMimeType() {
return '/image\/tiff/';
}
- protected function getImagickFormat(string $mimeType): string {
- return 'TIFF';
+ protected function hasExpectedMagicBytes(string $content): bool {
+ return $this->hasSignatureAt($content, "II*\0") || $this->hasSignatureAt($content, "MM\0*");
}
}
diff --git a/tests/lib/Preview/CoderPinningTest.php b/tests/lib/Preview/CoderPinningTest.php
deleted file mode 100644
index 2bf0dadf12c..00000000000
--- a/tests/lib/Preview/CoderPinningTest.php
+++ /dev/null
@@ -1,133 +0,0 @@
-
- *
- * @copyright Copyright (c) 2026, ownCloud GmbH
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see
- *
- */
-
-namespace Test\Preview;
-
-use Generator;
-use OC\Preview\Bitmap;
-use OC\Preview\Font;
-use OC\Preview\Heic;
-use OC\Preview\Illustrator;
-use OC\Preview\PDF;
-use OC\Preview\Photoshop;
-use OC\Preview\Postscript;
-use OC\Preview\SGI;
-use OC\Preview\TIFF;
-use OCP\Files\File;
-use Test\TestCase;
-
-class CoderPinningTest extends TestCase {
- private function makeFile(string $content, string $mimeType): File {
- $stream = \fopen('php://memory', 'rb+');
- \fwrite($stream, $content);
- \rewind($stream);
- $file = $this->createMock(File::class);
- $file->method('fopen')->willReturn($stream);
- $file->method('getMimeType')->willReturn($mimeType);
- return $file;
- }
-
- /**
- * @dataProvider providesLegitimateContent
- */
- public function testDecodesItsOwnFormat(string $fixture, string $mimeType, Bitmap $provider): void {
- if (\count(\Imagick::queryFormats('SVG')) === 0) {
- $this->markTestSkipped('No SVG/extra ImageMagick coders present');
- }
- # HEIF is registered as a coder distinct from HEIC on a real libheif1 install (verified
- # against the actual owncloud/server images), but not in every CI ImageMagick build.
- if ($mimeType === 'image/heif' && \count(\Imagick::queryFormats('HEIF')) === 0) {
- $this->markTestSkipped('No distinct HEIF coder present in this environment');
- }
- $content = \file_get_contents(__DIR__ . '/../../data/' . $fixture);
- $file = $this->makeFile($content, $mimeType);
-
- $result = $provider->getThumbnail($file, 32, 32, false);
-
- $this->assertNotFalse($result, "$fixture via " . \get_class($provider) . ' should have decoded');
- }
-
- public function providesLegitimateContent(): Generator {
- yield 'PDF' => ['testimage.pdf', 'application/pdf', new PDF()];
- yield 'Postscript (EPS)' => ['testimage.eps', 'application/postscript', new Postscript()];
- yield 'Illustrator (AI)' => ['testimage.ai', 'application/illustrator', new Illustrator()];
- yield 'Photoshop (PSD)' => ['testimage.psd', 'application/x-photoshop', new Photoshop()];
- yield 'SGI' => ['testimage.sgi', 'image/sgi', new SGI()];
- yield 'TIFF' => ['testimage.tiff', 'image/tiff', new TIFF()];
- # no genuine OTF ('OTTO'-tagged) fixture here: this environment's ImageMagick/FreeType
- # delegate cannot decode CFF-outline OpenType fonts at all, pinned or not - confirmed
- # against four real system .otf files. TTF-tagged content, which the TTF coder decodes
- # fine, is what's actually exercised in practice for the font-sfnt mime type.
- yield 'Font (font-sfnt, ttf bytes)' => ['testimage.ttf', 'application/font-sfnt', new Font()];
- yield 'Heic (image/heic)' => ['testimage.heic', 'image/heic', new Heic()];
- yield 'Heic (image/heif)' => ['testimage.heic', 'image/heif', new Heic()];
- }
-
- /**
- * PostScript content is sniffed by libmagic as application/postscript, which
- * isDangerousToDecode() must not reject since Postscript/PDF legitimately decode
- * it - so the mime-type gate alone lets it through here too. Pinning the expected
- * coder is what stops ImageMagick's own content-sniffing from handing it to the
- * Ghostscript delegate through a provider that has nothing to do with PostScript.
- *
- * PDF/Postscript/Illustrator are deliberately not in this set: they are the
- * Ghostscript-backed providers PostScript-ish content is NOT foreign to, so
- * feeding it to them tests Ghostscript's own leniency, not cross-coder confusion.
- * Font is also excluded: see testFontNeverInvokesADangerousCoderForForeignContent().
- *
- * @dataProvider providesForeignProviders
- */
- public function testRejectsPostScriptContentFromAForeignProvider(Bitmap $provider, string $mimeType): void {
- if (\count(\Imagick::queryFormats('SVG')) === 0) {
- $this->markTestSkipped('No SVG/extra ImageMagick coders present');
- }
- $postscript = "%!PS-Adobe-3.0\n%%BoundingBox: 0 0 10 10\nshowpage\n";
- $file = $this->makeFile($postscript, $mimeType);
-
- $result = $provider->getThumbnail($file, 32, 32, false);
-
- $this->assertFalse($result);
- }
-
- public function providesForeignProviders(): Generator {
- yield 'SGI' => [new SGI(), 'image/sgi'];
- yield 'Photoshop' => [new Photoshop(), 'application/x-photoshop'];
- yield 'TIFF' => [new TIFF(), 'image/tiff'];
- yield 'Heic' => [new Heic(), 'image/heic'];
- }
-
- public function testFontNeverInvokesADangerousCoderForForeignContent(): void {
- if (\count(\Imagick::queryFormats('SVG')) === 0) {
- $this->markTestSkipped('No SVG/extra ImageMagick coders present');
- }
- $postscript = "%!PS-Adobe-3.0\n%%BoundingBox: 0 0 10 10\nshowpage\n";
- $file = $this->makeFile($postscript, 'application/font-sfnt');
-
- $result = (new Font())->getThumbnail($file, 32, 32, false);
-
- # FreeType fails on non-font bytes by producing a blank placeholder, not by
- # invoking Ghostscript or a script coder - so this may be a small valid image
- # rather than false, but it must never carry rendered PostScript content.
- if ($result !== false) {
- $this->assertLessThan(2048, \strlen($result->data()));
- }
- }
-}
diff --git a/tests/lib/Preview/MagicByteGateTest.php b/tests/lib/Preview/MagicByteGateTest.php
new file mode 100644
index 00000000000..7fab05b3801
--- /dev/null
+++ b/tests/lib/Preview/MagicByteGateTest.php
@@ -0,0 +1,194 @@
+
+ *
+ * @copyright Copyright (c) 2026, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace Test\Preview;
+
+use Generator;
+use OC\Preview\Bitmap;
+use OC\Preview\Font;
+use OC\Preview\Heic;
+use OC\Preview\Illustrator;
+use OC\Preview\PDF;
+use OC\Preview\Photoshop;
+use OC\Preview\Postscript;
+use OC\Preview\SGI;
+use OC\Preview\TIFF;
+use OCP\Files\File;
+use ReflectionMethod;
+use Test\TestCase;
+
+class MagicByteGateTest extends TestCase {
+ private function makeFile(string $content, string $mimeType): File {
+ $stream = \fopen('php://memory', 'rb+');
+ \fwrite($stream, $content);
+ \rewind($stream);
+ $file = $this->createMock(File::class);
+ $file->method('fopen')->willReturn($stream);
+ $file->method('getMimeType')->willReturn($mimeType);
+ return $file;
+ }
+
+ /**
+ * @dataProvider providesLegitimateContent
+ */
+ public function testDecodesItsOwnFormat(string $fixture, string $mimeType, Bitmap $provider): void {
+ if (\count(\Imagick::queryFormats('SVG')) === 0) {
+ $this->markTestSkipped('No SVG/extra ImageMagick coders present');
+ }
+ # HEIF is registered as a coder distinct from HEIC on a real libheif1 install (verified
+ # against the actual owncloud/server images), but not in every CI ImageMagick build.
+ if ($mimeType === 'image/heif' && \count(\Imagick::queryFormats('HEIF')) === 0) {
+ $this->markTestSkipped('No distinct HEIF coder present in this environment');
+ }
+ $content = \file_get_contents(__DIR__ . '/../../data/' . $fixture);
+ $file = $this->makeFile($content, $mimeType);
+
+ $result = $provider->getThumbnail($file, 32, 32, false);
+
+ $this->assertNotFalse($result, "$fixture via " . \get_class($provider) . ' should have decoded');
+ }
+
+ public function providesLegitimateContent(): Generator {
+ yield 'PDF' => ['testimage.pdf', 'application/pdf', new PDF()];
+ yield 'Postscript (EPS)' => ['testimage.eps', 'application/postscript', new Postscript()];
+ yield 'Illustrator (AI)' => ['testimage.ai', 'application/illustrator', new Illustrator()];
+ yield 'Photoshop (PSD)' => ['testimage.psd', 'application/x-photoshop', new Photoshop()];
+ yield 'SGI' => ['testimage.sgi', 'image/sgi', new SGI()];
+ yield 'TIFF' => ['testimage.tiff', 'image/tiff', new TIFF()];
+ # no genuine OTF ('OTTO'-tagged) fixture here: this environment's ImageMagick/FreeType
+ # delegate cannot decode CFF-outline OpenType fonts at all - confirmed against four
+ # real system .otf files. TTF-tagged content is what's actually exercised in practice
+ # for the font-sfnt mime type it shares with OTF.
+ yield 'Font (font-sfnt, ttf bytes)' => ['testimage.ttf', 'application/font-sfnt', new Font()];
+ yield 'Heic (image/heic)' => ['testimage.heic', 'image/heic', new Heic()];
+ yield 'Heic (image/heif)' => ['testimage.heic', 'image/heif', new Heic()];
+ }
+
+ /**
+ * PostScript content is sniffed by libmagic as application/postscript, which
+ * isDangerousToDecode() must not reject since Postscript/PDF legitimately decode
+ * it - so the mime-type gate alone lets it through here too. hasExpectedMagicBytes()
+ * is what rejects it before Imagick is ever invoked, for a provider that has
+ * nothing to do with PostScript.
+ *
+ * PDF/Postscript/Illustrator are deliberately not in this set: they are the
+ * Ghostscript-backed providers PostScript-ish content is NOT foreign to.
+ *
+ * @dataProvider providesForeignProviders
+ */
+ public function testRejectsPostScriptContentFromAForeignProvider(Bitmap $provider, string $mimeType): void {
+ $postscript = "%!PS-Adobe-3.0\n%%BoundingBox: 0 0 10 10\nshowpage\n";
+ $file = $this->makeFile($postscript, $mimeType);
+
+ $result = $provider->getThumbnail($file, 32, 32, false);
+
+ $this->assertFalse($result);
+ }
+
+ public function providesForeignProviders(): Generator {
+ yield 'SGI' => [new SGI(), 'image/sgi'];
+ yield 'Photoshop' => [new Photoshop(), 'application/x-photoshop'];
+ yield 'TIFF' => [new TIFF(), 'image/tiff'];
+ yield 'Heic' => [new Heic(), 'image/heic'];
+ # Font's gate rejects this deterministically too: "%!" matches none of its
+ # accepted sfnt tags or the PFB marker, so it never reaches FreeType at all.
+ yield 'Font' => [new Font(), 'application/font-sfnt'];
+ }
+
+ /**
+ * Stronger proof than garbage bytes: a GENUINE fixture of one format, fed to a
+ * provider for a different format. If hasExpectedMagicBytes() were accidentally
+ * missing or a no-op, several of these would very plausibly still decode -
+ * Imagick would happily auto-sniff a real PSD/TIFF/SGI/HEIC file regardless of
+ * which PHP class read it.
+ *
+ * @dataProvider providesForeignGenuineContent
+ */
+ public function testRejectsGenuineContentOfAnotherFormat(string $fixture, Bitmap $provider, string $mimeType): void {
+ $content = \file_get_contents(__DIR__ . '/../../data/' . $fixture);
+ $file = $this->makeFile($content, $mimeType);
+
+ $result = $provider->getThumbnail($file, 32, 32, false);
+
+ $this->assertFalse($result);
+ }
+
+ public function providesForeignGenuineContent(): Generator {
+ yield 'PSD fixture via SGI provider' => ['testimage.psd', new SGI(), 'image/sgi'];
+ yield 'TIFF fixture via Photoshop provider' => ['testimage.tiff', new Photoshop(), 'application/x-photoshop'];
+ yield 'SGI fixture via TIFF provider' => ['testimage.sgi', new TIFF(), 'image/tiff'];
+ yield 'HEIC fixture via Photoshop provider' => ['testimage.heic', new Photoshop(), 'application/x-photoshop'];
+ }
+
+ /**
+ * Direct boundary tests for hasExpectedMagicBytes() itself - the cheapest, most
+ * direct regression guard for this change, independent of any Imagick delegate
+ * being installed (rejection happens before Imagick is ever touched).
+ *
+ * @dataProvider providesMagicByteBoundaries
+ */
+ public function testHasExpectedMagicBytesBoundary(Bitmap $provider, string $content, bool $expected): void {
+ $method = new ReflectionMethod($provider, 'hasExpectedMagicBytes');
+ $method->setAccessible(true);
+
+ $this->assertSame($expected, $method->invoke($provider, $content));
+ }
+
+ public function providesMagicByteBoundaries(): Generator {
+ yield 'empty string rejected everywhere (PDF)' => [new PDF(), '', false];
+ yield 'single byte rejected (SGI)' => [new SGI(), "\x01", false];
+
+ yield 'PDF accepts %PDF-' => [new PDF(), "%PDF-1.4\n", true];
+ yield 'PDF rejects %!' => [new PDF(), "%!PS-Adobe-3.0\n", false];
+
+ yield 'Postscript accepts %!' => [new Postscript(), "%!PS-Adobe-3.0\n", true];
+ yield 'Postscript rejects %PDF-' => [new Postscript(), "%PDF-1.4\n", false];
+
+ yield 'Illustrator accepts %PDF-' => [new Illustrator(), "%PDF-1.4\n", true];
+ yield 'Illustrator accepts %!' => [new Illustrator(), "%!PS-Adobe-3.0\n", true];
+ yield 'Illustrator rejects 8BPS' => [new Illustrator(), "8BPS\0\0", false];
+
+ yield 'Photoshop accepts 8BPS' => [new Photoshop(), "8BPS\x00\x01", true];
+ yield 'Photoshop rejects %PDF-' => [new Photoshop(), "%PDF-1.4\n", false];
+
+ yield 'SGI accepts 0x01 0xDA' => [new SGI(), "\x01\xDA\x01\x01", true];
+ yield 'SGI rejects 8BPS' => [new SGI(), "8BPS\x00\x01", false];
+
+ yield 'TIFF accepts little-endian II*\0' => [new TIFF(), "II*\0\x08\x00\x00\x00", true];
+ yield 'TIFF accepts big-endian MM\0*' => [new TIFF(), "MM\0*\x00\x00\x00\x08", true];
+ yield 'TIFF rejects 8BPS' => [new TIFF(), "8BPS\x00\x01", false];
+
+ yield 'Font accepts sfnt 1.0 tag' => [new Font(), "\x00\x01\x00\x00rest", true];
+ yield 'Font accepts OTTO tag' => [new Font(), 'OTTOrest', true];
+ yield 'Font accepts true tag' => [new Font(), 'truerest', true];
+ yield 'Font accepts ttcf tag' => [new Font(), 'ttcfrest', true];
+ yield 'Font accepts PFB ASCII segment' => [new Font(), "\x80\x01rest", true];
+ yield 'Font accepts PFB binary segment' => [new Font(), "\x80\x02rest", true];
+ yield 'Font accepts PFB EOF segment' => [new Font(), "\x80\x03rest", true];
+ yield 'Font rejects PFB with an unknown segment type' => [new Font(), "\x80\x04rest", false];
+ yield 'Font rejects %!' => [new Font(), "%!PS-Adobe-3.0\n", false];
+
+ yield 'Heic accepts a heic brand' => [new Heic(), "\x00\x00\x00\x18ftypheic", true];
+ yield 'Heic accepts a mif1 (generic HEIF) brand' => [new Heic(), "\x00\x00\x00\x18ftypmif1", true];
+ yield 'Heic rejects an avif brand' => [new Heic(), "\x00\x00\x00\x18ftypavif", false];
+ yield 'Heic rejects content missing the ftyp box' => [new Heic(), "\x00\x00\x00\x18wxyzheic", false];
+ }
+}
From a749dde426944bfe0b7976d394cee24ed8e11fd9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?=
<323649642+oc-tmueller@users.noreply.github.com>
Date: Mon, 14 Sep 2026 12:40:24 +0200
Subject: [PATCH 2/3] docs: add changelog entry for OC10-164 magic-byte gate
fix (#41833)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
---
changelog/unreleased/41833 | 10 ++++++++++
1 file changed, 10 insertions(+)
create mode 100644 changelog/unreleased/41833
diff --git a/changelog/unreleased/41833 b/changelog/unreleased/41833
new file mode 100644
index 00000000000..336caae79ab
--- /dev/null
+++ b/changelog/unreleased/41833
@@ -0,0 +1,10 @@
+Security: Replace bitmap preview Imagick coder pinning with a magic-byte check
+
+The previous hardening for bitmap previews pinned the ImageMagick coder for
+each provider by writing the file's content to a temporary file before
+decoding, which added a disk write to every preview generation. Each
+provider now instead verifies its own expected file signature against the
+raw content in memory before ImageMagick is invoked at all, closing the
+same class of cross-provider decoding confusion without the extra I/O.
+
+https://github.com/owncloud/core/pull/41833
From 121ba3882ff7e22c2b0d44cbbadd6a72fcd1461e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?=
<323649642+oc-tmueller@users.noreply.github.com>
Date: Mon, 14 Sep 2026 15:11:36 +0200
Subject: [PATCH 3/3] fix: verify magic-byte patterns against ImageMagick's
real magic table (OC10-164)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
#41833's patterns were derived from documented format specs and my own
fixture testing, disclosed there as "not yet cross-checked against
ImageMagick's own magic.c table". Fetched the exact upstream source (Ubuntu
22.04's imagemagick source package, 6.9.11.60+dfsg, via apt-get source) and
diffed every pattern against magick/magic.c's compiled-in MagicMap[] - the
actual table SetImageInfo() consults for unpinned content-sniffing. Six
real discrepancies found, all fixed:
- Postscript/Illustrator only accepted plain "%!". ImageMagick's "PS" entry
also matches "\004%!" (DOS EPS ASCII) and "\305\320\323\306" (DOS EPS
binary preamble) - real, still-encountered Windows-generated EPS
variants. Added both via a new shared hasPostScriptSignature() helper.
- Font's TTF check used the 4-byte conceptual sfnt version tag; the real
entry is 5 bytes (the high byte of numTables, which is 0 for any font
under 256 tables - true of every real font, but the previous 4-byte
check was one byte short of what ImageMagick itself requires).
- Font accepted "OTTO"/"true"/"ttcf" - none of the three have an entry in
ImageMagick's magic table at all, matching what this investigation
already found empirically (no decode delegate for genuine OTF content,
pinned or not). Removed as dead weight that matched nothing real.
- Font's PFB check was wrong: it validated the generic 0x80+segment-byte
framing every PFB file has, not what ImageMagick's own "PFB" entry
actually requires - "%!PS-AdobeFont-1.0" at offset 6, i.e. that a valid
Adobe Type 1 font program specifically follows the framing. Fixed to the
real pattern.
- Heic's brand allow-list had 9 entries; only 4 ("heic", "heix", "avif",
"mif1") are registered in ImageMagick's table under the "HEIC" coder
name. Notably "avif" was missing - previously excluded on the assumption
AVIF is a distinct format ImageMagick sniffs separately, which is wrong:
ImageMagick's own table classifies avif-branded content as "HEIC".
Fixed the list to match exactly; dropped the five entries that don't
correspond to any real ImageMagick pattern.
- TIFF didn't accept BigTIFF ("TIFF64" in ImageMagick's table, distinct
4/8-byte patterns from classic TIFF). BigTIFF decoded fine through the
original, unpinned readImageBlob() this whole change is built on top of,
so omitting it would have been a real functional regression for that
format, not just an unverified edge case. Added.
None of these were security gaps - every one was either a false rejection
(a real format's own provider would have rejected valid content) or dead,
unreachable code (accepting a signature ImageMagick itself never sniffs,
which just fails safely downstream with "no decode delegate"). Confirmed
directly: MVG/MSL/MSVG have no entry in ImageMagick's magic table at all,
so unpinned content-sniffing can never route into them regardless of what
any provider's check accepts - the cross-coder-confusion class this whole
line of work closes was never at risk from any of these six findings.
Verified in a fresh owncloudci/php:8.3 container: full tests/lib/Preview/
suite green (109 tests, 263 assertions, 0 failures, 9 unrelated skips).
Signed-off-by: Thomas Müller <323649642+oc-tmueller@users.noreply.github.com>
---
lib/private/Preview/Bitmap.php | 16 +++++++++++++++
lib/private/Preview/Font.php | 23 ++++++++++++++--------
lib/private/Preview/Heic.php | 5 ++++-
lib/private/Preview/Illustrator.php | 4 ++--
lib/private/Preview/Postscript.php | 2 +-
lib/private/Preview/TIFF.php | 12 +++++++++++-
tests/lib/Preview/MagicByteGateTest.php | 26 ++++++++++++++-----------
7 files changed, 64 insertions(+), 24 deletions(-)
diff --git a/lib/private/Preview/Bitmap.php b/lib/private/Preview/Bitmap.php
index 37f87e81024..3379b2cf476 100644
--- a/lib/private/Preview/Bitmap.php
+++ b/lib/private/Preview/Bitmap.php
@@ -151,6 +151,22 @@ protected function hasSignatureAt(string $content, string $signature, int $offse
return \substr($content, $offset, \strlen($signature)) === $signature;
}
+ /**
+ * True if $content is one of the three PostScript variants ImageMagick's own
+ * magic table (magick/magic.c, MagicMap[], coder name "PS") recognizes: plain
+ * ASCII PostScript, DOS EPS ASCII (a leading Ctrl-D byte), or DOS EPS binary
+ * (a 4-byte binary preamble). Shared by Postscript and Illustrator, since
+ * Illustrator's pre-9, non-PDF-based files use a plain PostScript header too.
+ */
+ protected function hasPostScriptSignature(string $content): bool {
+ foreach (['%!', "\x04%!", "\xC5\xD0\xD3\xC6"] as $signature) {
+ if ($this->hasSignatureAt($content, $signature)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Returns a resized \Imagick object
*
diff --git a/lib/private/Preview/Font.php b/lib/private/Preview/Font.php
index 08b8b5ce7c3..a20a926cdc9 100644
--- a/lib/private/Preview/Font.php
+++ b/lib/private/Preview/Font.php
@@ -31,14 +31,21 @@ public function getMimeType() {
}
protected function hasExpectedMagicBytes(string $content): bool {
- foreach (["\x00\x01\x00\x00", 'OTTO', 'true', 'ttcf'] as $sfntTag) {
- if ($this->hasSignatureAt($content, $sfntTag)) {
- return true;
- }
+ # Verified against ImageMagick's own compiled-in magic table
+ # (magick/magic.c, MagicMap[]): the sfnt version-1.0 tag is the ONLY font
+ # signature it recognizes ("TTF", 5 bytes including the high byte of
+ # numTables, which is 0 for any font with fewer than 256 tables - true in
+ # practice for every real font). It has no entry at all for "OTTO"/"true"/
+ # "ttcf" - confirmed empirically too: this environment's Imagick has no
+ # decode delegate for genuine OTF ('OTTO'-tagged) content regardless of
+ # how it's read, pinned or not.
+ if ($this->hasSignatureAt($content, "\x00\x01\x00\x00\x00")) {
+ return true;
}
- # PFB (Printer Font Binary): each segment starts with 0x80 followed by a
- # segment-type byte (0x01 ASCII, 0x02 binary, 0x03 EOF).
- return \strlen($content) >= 2 && $content[0] === "\x80"
- && \in_array($content[1], ["\x01", "\x02", "\x03"], true);
+ # PFB ("Printer Font Binary"): the entry is "PFB", offset 6,
+ # "%!PS-AdobeFont-1.0" - the first 6 bytes are the PFB binary segment
+ # header (0x80, segment type, 4-byte little-endian length), followed by
+ # the standard Adobe Type 1 font program identification string.
+ return $this->hasSignatureAt($content, '%!PS-AdobeFont-1.0', 6);
}
}
diff --git a/lib/private/Preview/Heic.php b/lib/private/Preview/Heic.php
index 156f5c690f5..48b72014415 100644
--- a/lib/private/Preview/Heic.php
+++ b/lib/private/Preview/Heic.php
@@ -33,10 +33,13 @@ public function getMimeType() {
protected function hasExpectedMagicBytes(string $content): bool {
// ISO-BMFF: bytes[0:4] are a variable box size (not checked - any value is
// structurally valid), bytes[4:8] must be "ftyp", bytes[8:12] are the brand.
+ // Brand list verified against ImageMagick's own compiled-in magic table
+ // (magick/magic.c, MagicMap[]): "avif"/"heic"/"heix"/"mif1" are registered
+ // there under the "HEIC" coder name - no others are.
if (!$this->hasSignatureAt($content, 'ftyp', 4)) {
return false;
}
$brand = \substr($content, 8, 4);
- return \in_array($brand, ['heic', 'heix', 'hevc', 'heim', 'heis', 'hevm', 'hevs', 'mif1', 'msf1'], true);
+ return \in_array($brand, ['avif', 'heic', 'heix', 'mif1'], true);
}
}
diff --git a/lib/private/Preview/Illustrator.php b/lib/private/Preview/Illustrator.php
index 59e9396365f..72f1b0f79c8 100644
--- a/lib/private/Preview/Illustrator.php
+++ b/lib/private/Preview/Illustrator.php
@@ -33,8 +33,8 @@ public function getMimeType() {
protected function hasExpectedMagicBytes(string $content): bool {
// Modern (9+) Illustrator files are PDF-compatible ("%PDF-"); legacy pre-9
- // files use a plain PostScript header ("%!"). Both already reach the same
+ // files use a plain PostScript header. Both already reach the same
// Ghostscript/PDF delegate as the Postscript and PDF providers.
- return $this->hasSignatureAt($content, '%PDF-') || $this->hasSignatureAt($content, '%!');
+ return $this->hasSignatureAt($content, '%PDF-') || $this->hasPostScriptSignature($content);
}
}
diff --git a/lib/private/Preview/Postscript.php b/lib/private/Preview/Postscript.php
index 7507ec3a307..d112c2f41f7 100644
--- a/lib/private/Preview/Postscript.php
+++ b/lib/private/Preview/Postscript.php
@@ -32,6 +32,6 @@ public function getMimeType() {
}
protected function hasExpectedMagicBytes(string $content): bool {
- return $this->hasSignatureAt($content, '%!');
+ return $this->hasPostScriptSignature($content);
}
}
diff --git a/lib/private/Preview/TIFF.php b/lib/private/Preview/TIFF.php
index 7cd2bce359d..20e5b839ad7 100644
--- a/lib/private/Preview/TIFF.php
+++ b/lib/private/Preview/TIFF.php
@@ -32,6 +32,16 @@ public function getMimeType() {
}
protected function hasExpectedMagicBytes(string $content): bool {
- return $this->hasSignatureAt($content, "II*\0") || $this->hasSignatureAt($content, "MM\0*");
+ // Classic TIFF, both byte orders, plus BigTIFF ("TIFF64" in ImageMagick's own
+ // magic table) - verified against magick/magic.c's MagicMap[]. BigTIFF was
+ // reachable via the original, unpinned readImageBlob() call this replaces, so
+ // leaving it out here would be a real functional regression, not just an
+ // unverified edge case.
+ foreach (["II*\0", "MM\0*", "II+\0\x08\x00\x00\x00", "MM\0+\x00\x08\x00\x00"] as $signature) {
+ if ($this->hasSignatureAt($content, $signature)) {
+ return true;
+ }
+ }
+ return false;
}
}
diff --git a/tests/lib/Preview/MagicByteGateTest.php b/tests/lib/Preview/MagicByteGateTest.php
index 7fab05b3801..a142fa7756a 100644
--- a/tests/lib/Preview/MagicByteGateTest.php
+++ b/tests/lib/Preview/MagicByteGateTest.php
@@ -159,11 +159,14 @@ public function providesMagicByteBoundaries(): Generator {
yield 'PDF accepts %PDF-' => [new PDF(), "%PDF-1.4\n", true];
yield 'PDF rejects %!' => [new PDF(), "%!PS-Adobe-3.0\n", false];
- yield 'Postscript accepts %!' => [new Postscript(), "%!PS-Adobe-3.0\n", true];
+ yield 'Postscript accepts plain %!' => [new Postscript(), "%!PS-Adobe-3.0\n", true];
+ yield 'Postscript accepts DOS EPS ASCII (0x04 %!)' => [new Postscript(), "\x04%!PS-Adobe-3.0\n", true];
+ yield 'Postscript accepts DOS EPS binary preamble' => [new Postscript(), "\xC5\xD0\xD3\xC6rest", true];
yield 'Postscript rejects %PDF-' => [new Postscript(), "%PDF-1.4\n", false];
yield 'Illustrator accepts %PDF-' => [new Illustrator(), "%PDF-1.4\n", true];
- yield 'Illustrator accepts %!' => [new Illustrator(), "%!PS-Adobe-3.0\n", true];
+ yield 'Illustrator accepts plain %!' => [new Illustrator(), "%!PS-Adobe-3.0\n", true];
+ yield 'Illustrator accepts DOS EPS binary preamble' => [new Illustrator(), "\xC5\xD0\xD3\xC6rest", true];
yield 'Illustrator rejects 8BPS' => [new Illustrator(), "8BPS\0\0", false];
yield 'Photoshop accepts 8BPS' => [new Photoshop(), "8BPS\x00\x01", true];
@@ -174,21 +177,22 @@ public function providesMagicByteBoundaries(): Generator {
yield 'TIFF accepts little-endian II*\0' => [new TIFF(), "II*\0\x08\x00\x00\x00", true];
yield 'TIFF accepts big-endian MM\0*' => [new TIFF(), "MM\0*\x00\x00\x00\x08", true];
+ yield 'TIFF accepts little-endian BigTIFF (II+\0)' => [new TIFF(), "II+\0\x08\x00\x00\x00", true];
+ yield 'TIFF accepts big-endian BigTIFF (MM\0+)' => [new TIFF(), "MM\0+\x00\x08\x00\x00", true];
yield 'TIFF rejects 8BPS' => [new TIFF(), "8BPS\x00\x01", false];
- yield 'Font accepts sfnt 1.0 tag' => [new Font(), "\x00\x01\x00\x00rest", true];
- yield 'Font accepts OTTO tag' => [new Font(), 'OTTOrest', true];
- yield 'Font accepts true tag' => [new Font(), 'truerest', true];
- yield 'Font accepts ttcf tag' => [new Font(), 'ttcfrest', true];
- yield 'Font accepts PFB ASCII segment' => [new Font(), "\x80\x01rest", true];
- yield 'Font accepts PFB binary segment' => [new Font(), "\x80\x02rest", true];
- yield 'Font accepts PFB EOF segment' => [new Font(), "\x80\x03rest", true];
- yield 'Font rejects PFB with an unknown segment type' => [new Font(), "\x80\x04rest", false];
+ yield 'Font accepts sfnt 1.0 tag' => [new Font(), "\x00\x01\x00\x00\x00rest", true];
+ yield 'Font rejects sfnt tag with a non-zero 5th byte' => [new Font(), "\x00\x01\x00\x00\x01rest", false];
+ yield 'Font rejects OTTO (no ImageMagick magic entry for it)' => [new Font(), 'OTTOrest', false];
+ yield 'Font accepts a real PFB header' => [new Font(), "\x80\x01\x00\x00\x00\x00%!PS-AdobeFont-1.0", true];
+ yield 'Font rejects PFB-looking bytes without the Adobe font string' => [new Font(), "\x80\x01\x00\x00\x00\x00not a font", false];
yield 'Font rejects %!' => [new Font(), "%!PS-Adobe-3.0\n", false];
yield 'Heic accepts a heic brand' => [new Heic(), "\x00\x00\x00\x18ftypheic", true];
+ yield 'Heic accepts a heix brand' => [new Heic(), "\x00\x00\x00\x18ftypheix", true];
yield 'Heic accepts a mif1 (generic HEIF) brand' => [new Heic(), "\x00\x00\x00\x18ftypmif1", true];
- yield 'Heic rejects an avif brand' => [new Heic(), "\x00\x00\x00\x18ftypavif", false];
+ yield 'Heic accepts an avif brand' => [new Heic(), "\x00\x00\x00\x18ftypavif", true];
+ yield 'Heic rejects an unregistered brand' => [new Heic(), "\x00\x00\x00\x18ftyphevc", false];
yield 'Heic rejects content missing the ftyp box' => [new Heic(), "\x00\x00\x00\x18wxyzheic", false];
}
}