assertFalse($ref->invoke($preview, '/usr/bin/heif-convert') && (string) ini_get('open_basedir') !== '' && ! str_contains((string) ini_get('open_basedir'), '/usr/bin')); $resolved = (new \ReflectionMethod(PhotoPreview::class, 'resolveBinary'))->invoke($preview, 'heif-convert'); $this->assertNotNull($resolved); } #[Test] public function detects_heic_ftyp_header(): void { $preview = new PhotoPreview; $heicHead = "\x00\x00\x00\x28ftypheic\x00\x00\x00\x00mif1"; $this->assertTrue($preview->headLooksHeic($heicHead)); $this->assertFalse($preview->headLooksHeic("\xFF\xD8\xFF\xD9")); $this->assertFalse($preview->headLooksHeic('')); } #[Test] public function heic_convert_process_throw_does_not_bubble(): void { Storage::fake('local'); Process::fake(function () { throw new \RuntimeException('proc_open is disabled'); }); $abs = sys_get_temp_dir().'/preview_'.uniqid().'.heic'; file_put_contents($abs, "\x00\x00\x00\x18ftypheic\x00\x00\x00\x00mif1"); $out = (new PhotoPreview)->payload($abs, 'dev-prev', hash('sha256', 'throw')); $this->assertFalse($out['converted']); $this->assertNotSame('', $out['bytes']); @unlink($abs); } #[Test] public function heic_without_throwing_when_system_bins_are_outside_basedir(): void { Storage::fake('local'); $abs = sys_get_temp_dir().'/preview_'.uniqid().'.heic'; file_put_contents($abs, "\x00\x00\x00\x18ftypheic\x00\x00\x00\x00mif1"); $out = (new PhotoPreview)->payload($abs, 'dev-prev', hash('sha256', 'heic-head')); $this->assertFalse($out['converted']); $this->assertNotSame('', $out['bytes']); @unlink($abs); } #[Test] public function jpeg_passthrough_does_not_convert(): void { Storage::fake('local'); $abs = $this->writeJpeg(); $out = (new PhotoPreview)->payload($abs, 'dev-prev', hash('sha256', 'jpeg')); $this->assertFalse($out['converted']); $this->assertSame('image/jpeg', $out['mime']); $this->assertSame(file_get_contents($abs), $out['bytes']); @unlink($abs); } #[Test] public function heic_converts_to_jpeg_and_leaves_original(): void { if (! is_executable('/usr/bin/sips')) { $this->markTestSkipped('sips is required to generate and convert HEIC'); } Storage::fake('local'); $heic = $this->writeHeic(); $original = file_get_contents($heic); $this->assertNotFalse($original); $sha = hash('sha256', $original); $out = (new PhotoPreview)->payload($heic, 'dev-prev', $sha); $this->assertTrue($out['converted']); $this->assertSame('image/jpeg', $out['mime']); $this->assertSame("\xFF\xD8", substr($out['bytes'], 0, 2)); $this->assertSame($original, file_get_contents($heic)); Storage::disk('local')->assertExists('c2/photo-previews/dev-prev/'.$sha.'.jpg'); @unlink($heic); } private function writeJpeg(): string { $path = sys_get_temp_dir().'/preview_'.uniqid().'.jpg'; $im = imagecreatetruecolor(8, 8); imagefilledrectangle($im, 0, 0, 7, 7, imagecolorallocate($im, 255, 255, 255)); imagejpeg($im, $path, 90); return $path; } private function writeHeic(): string { $jpg = $this->writeJpeg(); $heic = sys_get_temp_dir().'/preview_'.uniqid().'.heic'; $result = Process::timeout(20)->run(['/usr/bin/sips', '-s', 'format', 'heic', '--out', $heic, $jpg]); @unlink($jpg); if (! $result->successful() || ! is_file($heic)) { $this->markTestSkipped('sips could not write a HEIC fixture'); } return $heic; } }