Skip to content

Commit f26ec41

Browse files
authored
Merge pull request #20 from LibreSign/fix/infection-uncovered-main
Cover remaining uncovered Infection mutants
2 parents 78e231f + 8979adf commit f26ec41

12 files changed

Lines changed: 245 additions & 5 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreSign
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreSign\XObjectTemplate\Pdf\Png;
9+
10+
/** @internal */
11+
final class PhpPngHeaderUnpacker implements PngHeaderUnpackerInterface
12+
{
13+
public function unpack(string $data): array|false
14+
{
15+
return unpack(
16+
'Nwidth/Nheight/CbitDepth/CcolorType/Ccompression/Cfilter/Cinterlace',
17+
$data,
18+
);
19+
}
20+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreSign
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreSign\XObjectTemplate\Pdf\Png;
9+
10+
/** @internal */
11+
final class PhpPngScanlineCompressor implements PngScanlineCompressorInterface
12+
{
13+
public function compress(string $scanlines): string|false
14+
{
15+
return gzcompress($scanlines);
16+
}
17+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreSign
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreSign\XObjectTemplate\Pdf\Png;
9+
10+
/** @internal */
11+
interface PngHeaderUnpackerInterface
12+
{
13+
/**
14+
* @return array{
15+
* width: int,
16+
* height: int,
17+
* bitDepth: int,
18+
* colorType: int,
19+
* compression: int,
20+
* filter: int,
21+
* interlace: int
22+
* }|false
23+
*/
24+
public function unpack(string $data): array|false;
25+
}

src/Pdf/Png/PngParser.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
/** @internal */
1515
final readonly class PngParser implements PngParserInterface
1616
{
17+
private PngHeaderUnpackerInterface $headerUnpacker;
18+
19+
public function __construct(?PngHeaderUnpackerInterface $headerUnpacker = null)
20+
{
21+
$this->headerUnpacker = $headerUnpacker ?? new PhpPngHeaderUnpacker();
22+
}
23+
1724
public function parse(string $contents): ParsedPngImage
1825
{
1926
$this->assertPngSignature($contents);
@@ -107,10 +114,7 @@ public function parseHeader(string $data): array
107114
throw new InvalidArgumentException('Unable to parse the PNG IHDR chunk.');
108115
}
109116

110-
$header = unpack(
111-
'Nwidth/Nheight/CbitDepth/CcolorType/Ccompression/Cfilter/Cinterlace',
112-
$data,
113-
);
117+
$header = $this->headerUnpacker->unpack($data);
114118
if (!is_array($header)) {
115119
throw new InvalidArgumentException('Unable to parse the PNG IHDR chunk.');
116120
}

src/Pdf/Png/PngPdfImageFactory.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@
2222
{
2323
private PngParserInterface $parser;
2424
private PngScanlineUnfiltererInterface $scanlineUnfilterer;
25+
private PngScanlineCompressorInterface $scanlineCompressor;
2526

2627
public function __construct(
2728
?PngParserInterface $parser = null,
2829
?PngScanlineUnfiltererInterface $scanlineUnfilterer = null,
30+
?PngScanlineCompressorInterface $scanlineCompressor = null,
2931
) {
3032
$this->parser = $parser ?? new PngParser();
3133
$this->scanlineUnfilterer = $scanlineUnfilterer ?? new PngScanlineUnfilterer();
34+
$this->scanlineCompressor = $scanlineCompressor ?? new PhpPngScanlineCompressor();
3235
}
3336

3437
public function create(string $contents): EmbeddedPdfImage
@@ -136,7 +139,7 @@ private function createImageDictionary(int $width, int $height, string $colorSpa
136139

137140
private function compressScanlines(string $scanlines): string
138141
{
139-
$compressed = gzcompress($scanlines);
142+
$compressed = $this->scanlineCompressor->compress($scanlines);
140143
if (!is_string($compressed)) {
141144
throw new InvalidArgumentException('PNG scanlines could not be compressed.');
142145
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreSign
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreSign\XObjectTemplate\Pdf\Png;
9+
10+
/** @internal */
11+
interface PngScanlineCompressorInterface
12+
{
13+
public function compress(string $scanlines): string|false;
14+
}

tests/Unit/Pdf/FilesystemImageSourceReaderTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@ public function run(callable $operation, string $message): mixed
4242
self::assertSame('converted-contents', $reader->read($path));
4343
}
4444

45+
public function testReadRejectsNonStringWarningConverterResult(): void
46+
{
47+
$reader = new FilesystemImageSourceReader(new class implements WarningToExceptionConverterInterface {
48+
public function run(callable $operation, string $message): mixed
49+
{
50+
return false;
51+
}
52+
});
53+
$path = $this->createTemporaryFile('png', 'disk-contents');
54+
55+
$this->expectException(\InvalidArgumentException::class);
56+
$this->expectExceptionMessage(sprintf('Failed to read image source "%s".', $path));
57+
58+
$reader->read($path);
59+
}
60+
4561
public function testReadRejectsMissingSources(): void
4662
{
4763
$reader = new FilesystemImageSourceReader();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreSign
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreSign\XObjectTemplate\Tests\Unit\Pdf\Png;
9+
10+
use LibreSign\XObjectTemplate\Pdf\Png\PhpPngHeaderUnpacker;
11+
use PHPUnit\Framework\TestCase;
12+
13+
final class PhpPngHeaderUnpackerTest extends TestCase
14+
{
15+
public function testUnpackReturnsStructuredHeaderFields(): void
16+
{
17+
$unpacker = new PhpPngHeaderUnpacker();
18+
19+
self::assertSame(
20+
[
21+
'width' => 3,
22+
'height' => 2,
23+
'bitDepth' => 8,
24+
'colorType' => 6,
25+
'compression' => 0,
26+
'filter' => 0,
27+
'interlace' => 0,
28+
],
29+
$unpacker->unpack(pack('NNCCCCC', 3, 2, 8, 6, 0, 0, 0)),
30+
);
31+
}
32+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreSign
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreSign\XObjectTemplate\Tests\Unit\Pdf\Png;
9+
10+
use LibreSign\XObjectTemplate\Pdf\Png\PhpPngScanlineCompressor;
11+
use PHPUnit\Framework\TestCase;
12+
13+
final class PhpPngScanlineCompressorTest extends TestCase
14+
{
15+
public function testCompressReturnsRoundTrippableCompressedScanlines(): void
16+
{
17+
$compressor = new PhpPngScanlineCompressor();
18+
$scanlines = "\x00\xff\x00\x00\x00\x00\xff\x00";
19+
20+
$compressed = $compressor->compress($scanlines);
21+
22+
self::assertIsString($compressed);
23+
self::assertSame($scanlines, gzuncompress($compressed));
24+
}
25+
}

tests/Unit/Pdf/Png/PngParserTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace LibreSign\XObjectTemplate\Tests\Unit\Pdf\Png;
99

10+
use LibreSign\XObjectTemplate\Pdf\Png\PngHeaderUnpackerInterface;
1011
use LibreSign\XObjectTemplate\Pdf\Png\PngParser;
1112
use LibreSign\XObjectTemplate\Tests\Support\PngFixtureFactory;
1213
use PHPUnit\Framework\TestCase;
@@ -157,6 +158,21 @@ public function testParseHeaderRejectsUnexpectedHeaderLength(): void
157158
$parser->parseHeader('short-header');
158159
}
159160

161+
public function testParseHeaderRejectsUnpackFailures(): void
162+
{
163+
$parser = new PngParser(new class implements PngHeaderUnpackerInterface {
164+
public function unpack(string $data): array|false
165+
{
166+
return false;
167+
}
168+
});
169+
170+
$this->expectException(\InvalidArgumentException::class);
171+
$this->expectExceptionMessage('Unable to parse the PNG IHDR chunk.');
172+
173+
$parser->parseHeader(pack('NNCCCCC', 1, 1, 8, 2, 0, 0, 0));
174+
}
175+
160176
public function testParseRejectsMissingTrailerChunkWhenTrailingBytesAreTooShort(): void
161177
{
162178
$parser = new PngParser();

0 commit comments

Comments
 (0)