Skip to content

Commit 7e3f7b3

Browse files
authored
Recover unserialize errors (#21)
#19
1 parent 5fa6952 commit 7e3f7b3

File tree

5 files changed

+134
-10
lines changed

5 files changed

+134
-10
lines changed

src/FileDatastore.php

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22

33
namespace olvlvl\ComposerAttributeCollector;
44

5+
use Composer\IO\IOInterface;
6+
57
use function file_exists;
68
use function file_get_contents;
79
use function file_put_contents;
10+
use function is_array;
811
use function is_dir;
912
use function mkdir;
13+
use function restore_error_handler;
1014
use function serialize;
15+
use function set_error_handler;
1116
use function unserialize;
1217

1318
use const DIRECTORY_SEPARATOR;
@@ -21,7 +26,8 @@ final class FileDatastore implements Datastore
2126
* @param non-empty-string $dir
2227
*/
2328
public function __construct(
24-
private string $dir
29+
private string $dir,
30+
private IOInterface $io,
2531
) {
2632
assert($dir !== '');
2733

@@ -38,8 +44,7 @@ public function get(string $key): array
3844
return [];
3945
}
4046

41-
/** @phpstan-ignore-next-line */
42-
return unserialize(file_get_contents($filename));
47+
return self::safeGet($filename);
4348
}
4449

4550
public function set(string $key, array $data): void
@@ -48,4 +53,36 @@ public function set(string $key, array $data): void
4853

4954
file_put_contents($filename, serialize($data));
5055
}
56+
57+
/**
58+
* @return mixed[]
59+
*/
60+
private function safeGet(string $filename): array
61+
{
62+
$str = file_get_contents($filename);
63+
64+
if ($str === false) {
65+
return [];
66+
}
67+
68+
$errored = false;
69+
70+
set_error_handler(function (int $errno, string $errstr) use (&$errored, $filename): bool {
71+
$errored = true;
72+
73+
$this->io->warning("Unable to unserialize cache item $filename: $errstr");
74+
75+
return true;
76+
});
77+
78+
$ar = unserialize($str);
79+
80+
restore_error_handler();
81+
82+
if ($errored || !is_array($ar)) {
83+
return [];
84+
}
85+
86+
return $ar;
87+
}
5188
}

src/Plugin.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public static function dump(
100100
AutoloadsBuilder $autoloadsBuilder = null,
101101
ClassMapBuilder $classMapBuilder = null
102102
): void {
103-
$datastore = self::buildDefaultDatastore();
103+
$datastore = self::buildDefaultDatastore($io);
104104
$autoloadsBuilder ??= new AutoloadsBuilder();
105105
$classMapGenerator = new MemoizeClassMapGenerator($datastore, $io);
106106
$classMapBuilder ??= new ClassMapBuilder($classMapGenerator);
@@ -141,13 +141,13 @@ public static function dump(
141141
file_put_contents($filepath, $code);
142142
}
143143

144-
private static function buildDefaultDatastore(): Datastore
144+
private static function buildDefaultDatastore(IOInterface $io): Datastore
145145
{
146146
$basePath = Platform::getCwd();
147147

148148
assert($basePath !== '');
149149

150-
return new FileDatastore($basePath . DIRECTORY_SEPARATOR . self::CACHE_DIR);
150+
return new FileDatastore($basePath . DIRECTORY_SEPARATOR . self::CACHE_DIR, $io);
151151
}
152152

153153
private static function renderElapsedTime(float $start): string

tests/ClassMapBuilderTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ final class ClassMapBuilderTest extends TestCase
2121
{
2222
public function testBuildClassMap(): void
2323
{
24+
$io = new NullIO();
2425
$sut = new ClassMapBuilder(
2526
new MemoizeClassMapGenerator(
26-
new FileDatastore(get_cache_dir()),
27-
new NullIO(),
27+
new FileDatastore(get_cache_dir(), $io),
28+
$io,
2829
)
2930
);
3031

tests/FileDatastoreTest.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace tests\olvlvl\ComposerAttributeCollector;
4+
5+
use Composer\IO\IOInterface;
6+
use olvlvl\ComposerAttributeCollector\FileDatastore;
7+
use PHPUnit\Framework\MockObject\MockObject;
8+
use PHPUnit\Framework\TestCase;
9+
10+
use function file_put_contents;
11+
12+
final class FileDatastoreTest extends TestCase
13+
{
14+
private const DIR = __DIR__ . '/sandbox/';
15+
private const KEY = 'file-datastore';
16+
17+
/**
18+
* @var MockObject&IOInterface
19+
*/
20+
private MockObject|IOInterface $io;
21+
private FileDatastore $sut;
22+
23+
protected function setUp(): void
24+
{
25+
$this->io = $this->createMock(IOInterface::class);
26+
$this->sut = new FileDatastore(self::DIR, $this->io);
27+
28+
parent::setUp();
29+
}
30+
31+
public function testValidUnserialize(): void
32+
{
33+
$str = 'a:3:{i:0;i:2;i:1;i:3;i:2;i:5;}';
34+
$expected = [ 2, 3, 5 ];
35+
$this->io
36+
->expects($this->never())
37+
->method('warning')
38+
->withAnyParameters();
39+
40+
self::write($str);
41+
42+
$actual = $this->sut->get(self::KEY);
43+
44+
$this->assertEquals($expected, $actual);
45+
}
46+
47+
public function testUnserializeWithMissingEnum(): void
48+
{
49+
$str = 'a:1:{i:0;E:12:"Priority:TOP";}';
50+
$expected = [];
51+
$this->io
52+
->expects($this->atLeastOnce()) // PHP 8.2 also complains that the class 'Priority' is not found
53+
->method('warning')
54+
->with($this->stringStartsWith("Unable to unserialize cache item"));
55+
56+
self::write($str);
57+
58+
$actual = $this->sut->get(self::KEY);
59+
60+
$this->assertEquals($expected, $actual);
61+
}
62+
63+
public function testUnserializeWithMissingClass(): void
64+
{
65+
$this->markTestSkipped("Don't know how to catch missing classes yet");
66+
67+
$str = 'a:1:{i:0;O:8:"Priority":1:{s:8:"priority";i:1;}}';
68+
$expected = [];
69+
$this->io
70+
->expects($this->atLeastOnce())
71+
->method('warning')
72+
->with($this->stringStartsWith("Unable to unserialize cache item"));
73+
74+
self::write($str);
75+
76+
$actual = $this->sut->get(self::KEY);
77+
78+
$this->assertEquals($expected, $actual);
79+
}
80+
81+
private static function write(string $str): void
82+
{
83+
file_put_contents(self::DIR . self::KEY, $str);
84+
}
85+
}

tests/MemoizeClassMapGeneratorTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,10 @@ private static function write(string $name, string $data): void
9898
*/
9999
private static function map(string $path): array
100100
{
101+
$io = new NullIO();
101102
$generator = new MemoizeClassMapGenerator(
102-
new FileDatastore(get_cache_dir()),
103-
new NullIO(),
103+
new FileDatastore(get_cache_dir(), $io),
104+
$io,
104105
);
105106

106107
$generator->scanPaths($path);

0 commit comments

Comments
 (0)