root = $this->tempDir(); $this->keys = KeyRing::generate(); $this->src = $this->tempDir(); for ($i = 0; $i < 6; $i++) { file_put_contents("$this->src/f$i", random_bytes(3 * 1048576)); } $repo = Repository::initWithKey(new LocalBackend($this->root), $this->keys, $this->keys->wrap('pw')); $this->snapshot = $repo->backupDirectory($this->src)['snapshot']; } public function testABlobThatCameInDamagedOnceIsReadAgain(): void { $backend = new GarblingBackend(new LocalBackend($this->root), streams: 1); $target = $this->tempDir(); $this->assertSame(6, Repository::openWithKey($backend, $this->keys)->restore($this->snapshot, $target)); $this->assertSame(self::hashTree($this->src), self::hashTree($target)); $this->assertSame(0, $backend->streams, 'the damaged read happened'); } public function testDataDamagedOnTheLocationStillStopsTheRestore(): void { $backend = new GarblingBackend(new LocalBackend($this->root), streams: 1000, ranges: 1000); $this->expectException(\Exception::class); Repository::openWithKey($backend, $this->keys)->restore($this->snapshot, $this->tempDir()); } } /** Pack reads that flip a byte: the first $streams pack streams and $ranges range reads. */ final class GarblingBackend implements IBackend { use \OCA\NgBackup\Backend\GetsOneByOne; public function __construct( private IBackend $inner, public int $streams = 0, public int $ranges = 0, ) { } public function put(string $path, $stream): void { $this->inner->put($path, $stream); } public function get(string $path) { $fh = $this->inner->get($path); if (!str_starts_with($path, 'packs/') || $this->streams <= 0) { return $fh; } $this->streams--; $data = stream_get_contents($fh); fclose($fh); for ($k = 100; $k < strlen($data); $k += 65536) { $data[$k] = chr(ord($data[$k]) ^ 0x55); } $out = fopen('php://temp', 'w+b'); fwrite($out, $data); rewind($out); return $out; } public function getRange(string $path, int $offset, int $length): string { $data = $this->inner->getRange($path, $offset, $length); if (str_starts_with($path, 'packs/') && $this->ranges > 0) { $this->ranges--; $data[0] = chr(ord($data[0]) ^ 0x55); } return $data; } public function exists(string $path): bool { return $this->inner->exists($path); } public function move(string $from, string $to): void { $this->inner->move($from, $to); } public function list(string $prefix): array { return $this->inner->list($prefix); } public function listFiles(string $dir): array { return $this->inner->listFiles($dir); } public function delete(string $path): void { $this->inner->delete($path); } }