Files
nextcloud-server-mirror/tests/lib/Files/Cache/CacheTest.php
Ferdinand Thiessen 9a5fa196b7 fix(files): carry the unencrypted size over when copying a cache entry
The size of an encrypted file lives in `unencrypted_size`, and both
CacheEntry::getUnencryptedSize() and FileInfo::getSize() prefer it over
`size`. Cache::copyFromCache() copied the `encrypted` mark without it, so
the copy of an encrypted file was marked encrypted with an unencrypted size
of 0 and reported as empty - in the web UI, to clients and for quota - until
something rescanned it.

Copy the unencrypted size alongside the encrypted version, and reset it when
the mark is dropped for a target that is not encrypted, where the size is
read from `size`.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-09-20 18:19:07 +02:00

1087 lines
40 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\Files\Cache;
use OC\Files\Cache\Cache;
use OC\Files\Cache\CacheDependencies;
use OC\Files\Cache\CacheEntry;
use OC\Files\Cache\QuerySearchHelper;
use OC\Files\Cache\Wrapper\CacheJail;
use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchQuery;
use OC\Files\Storage\Temporary;
use OC\SystemConfig;
use OC\User\DisplayNameCache;
use OC\User\User;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Cache\CacheEntriesRemovedEvent;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\IMimeTypeLoader;
use OCP\Files\Search\ISearchComparison;
use OCP\Files\Storage\IStorage;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\IDBConnection;
use OCP\ITagManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Server;
use Psr\Log\LoggerInterface;
class LongId extends Temporary {
#[\Override]
public function getId(): string {
return 'long:' . str_repeat('foo', 50) . parent::getId();
}
}
/**
* Class CacheTest
*
*
* @package Test\Files\Cache
*/
#[\PHPUnit\Framework\Attributes\Group('DB')]
class CacheTest extends \Test\TestCase {
/**
* @var Temporary $storage ;
*/
protected $storage;
/**
* @var Temporary $storage2 ;
*/
protected $storage2;
/**
* @var Cache $cache
*/
protected $cache;
/**
* @var Cache $cache2
*/
protected $cache2;
#[\Override]
protected function setUp(): void {
parent::setUp();
$this->storage = new Temporary([]);
$this->storage2 = new Temporary([]);
$this->cache = new Cache($this->storage);
$this->cache2 = new Cache($this->storage2);
$this->cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]);
$this->cache2->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]);
}
#[\Override]
protected function tearDown(): void {
if ($this->cache) {
$this->cache->clear();
}
parent::tearDown();
}
public function testGetNumericId(): void {
$this->assertNotNull($this->cache->getNumericStorageId());
}
public function testSimple(): void {
$file1 = 'foo';
$file2 = 'foo/bar';
$data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder'];
$data2 = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file'];
$this->assertFalse($this->cache->inCache($file1));
$this->assertEquals($this->cache->get($file1), null);
$id1 = $this->cache->put($file1, $data1);
$this->assertTrue($this->cache->inCache($file1));
$cacheData1 = $this->cache->get($file1);
foreach ($data1 as $key => $value) {
$this->assertEquals($value, $cacheData1[$key]);
}
$this->assertEquals($cacheData1['mimepart'], 'foo');
$this->assertEquals($cacheData1['fileid'], $id1);
$this->assertEquals($id1, $this->cache->getId($file1));
$this->assertFalse($this->cache->inCache($file2));
$id2 = $this->cache->put($file2, $data2);
$this->assertTrue($this->cache->inCache($file2));
$cacheData2 = $this->cache->get($file2);
foreach ($data2 as $key => $value) {
$this->assertEquals($value, $cacheData2[$key]);
}
$this->assertEquals($cacheData1['fileid'], $cacheData2['parent']);
$this->assertEquals($cacheData2['fileid'], $id2);
$this->assertEquals($id2, $this->cache->getId($file2));
$this->assertEquals($id1, $this->cache->getParentId($file2));
$newSize = 1050;
$newId2 = $this->cache->put($file2, ['size' => $newSize]);
$cacheData2 = $this->cache->get($file2);
$this->assertEquals($newId2, $id2);
$this->assertEquals($cacheData2['size'], $newSize);
$this->assertEquals($cacheData1, $this->cache->get($file1));
$this->cache->remove($file2);
$this->assertFalse($this->cache->inCache($file2));
$this->assertEquals($this->cache->get($file2), null);
$this->assertTrue($this->cache->inCache($file1));
$this->assertEquals($cacheData1, $this->cache->get($id1));
}
public function testCacheEntryGetters(): void {
$file1 = 'foo';
$data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/file'];
$id1 = $this->cache->put($file1, $data1);
$entry = $this->cache->get($file1);
$this->assertEquals($entry->getId(), $id1);
$this->assertEquals($entry->getStorageId(), $this->cache->getNumericStorageId());
$this->assertEquals($entry->getPath(), 'foo');
$this->assertEquals($entry->getName(), 'foo');
$this->assertEquals($entry->getMimeType(), 'foo/file');
$this->assertEquals($entry->getMimePart(), 'foo');
$this->assertEquals($entry->getSize(), 100);
$this->assertEquals($entry->getMTime(), 50);
$this->assertEquals($entry->getStorageMTime(), 50);
$this->assertEquals($entry->getEtag(), null);
$this->assertEquals($entry->getPermissions(), 0);
$this->assertEquals($entry->isEncrypted(), false);
$this->assertEquals($entry->getMetadataEtag(), null);
$this->assertEquals($entry->getCreationTime(), null);
$this->assertEquals($entry->getUploadTime(), null);
$this->assertEquals($entry->getUnencryptedSize(), 100);
}
public function testGetUnencryptedSizeEncryptedZeroByte(): void {
$file1 = 'encrypted_zero';
$this->cache->put($file1, ['size' => 8192, 'mtime' => 50, 'mimetype' => 'application/octet-stream', 'encrypted' => 1, 'unencrypted_size' => 0]);
$entry = $this->cache->get($file1);
// getUnencryptedSize() must return 0 (the true plaintext size), not 8192 (the encrypted on-disk size)
$this->assertEquals(0, $entry->getUnencryptedSize());
$this->assertTrue($entry->isEncrypted());
}
public function testPartial(): void {
$file1 = 'foo';
$this->cache->put($file1, ['size' => 10]);
$this->assertEquals(new CacheEntry(['size' => 10]), $this->cache->get($file1));
$this->cache->put($file1, ['mtime' => 15]);
$this->assertEquals(new CacheEntry(['size' => 10, 'mtime' => 15]), $this->cache->get($file1));
$this->cache->put($file1, ['size' => 12]);
$this->assertEquals(new CacheEntry(['size' => 12, 'mtime' => 15]), $this->cache->get($file1));
}
#[\PHPUnit\Framework\Attributes\DataProvider('folderDataProvider')]
public function testFolder($folder): void {
if (strpos($folder, 'F09F9890')) {
// 4 byte UTF doesn't work on mysql
$params = Server::get(\OC\DB\Connection::class)->getParams();
if (Server::get(IDBConnection::class)->getDatabaseProvider() === IDBConnection::PLATFORM_MYSQL && $params['charset'] !== 'utf8mb4') {
$this->markTestSkipped('MySQL doesn\'t support 4 byte UTF-8');
}
}
$file2 = $folder . '/bar';
$file3 = $folder . '/foo';
$data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
$fileData = [];
$fileData['bar'] = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file'];
$fileData['foo'] = ['size' => 20, 'mtime' => 25, 'mimetype' => 'foo/file'];
$this->cache->put($folder, $data1);
$this->cache->put($file2, $fileData['bar']);
$this->cache->put($file3, $fileData['foo']);
$content = $this->cache->getFolderContents($folder);
$this->assertEquals(count($content), 2);
foreach ($content as $cachedData) {
$data = $fileData[$cachedData['name']];
foreach ($data as $name => $value) {
$this->assertEquals($value, $cachedData[$name]);
}
}
$file4 = $folder . '/unkownSize';
$fileData['unkownSize'] = ['size' => -1, 'mtime' => 25, 'mimetype' => 'foo/file'];
$this->cache->put($file4, $fileData['unkownSize']);
$this->assertEquals(-1, $this->cache->calculateFolderSize($folder));
$fileData['unkownSize'] = ['size' => 5, 'mtime' => 25, 'mimetype' => 'foo/file'];
$this->cache->put($file4, $fileData['unkownSize']);
$this->assertEquals(1025, $this->cache->calculateFolderSize($folder));
$this->cache->remove($file2);
$this->cache->remove($file3);
$this->cache->remove($file4);
$this->assertEquals(0, $this->cache->calculateFolderSize($folder));
$this->cache->remove($folder);
$this->assertFalse($this->cache->inCache($folder . '/foo'));
$this->assertFalse($this->cache->inCache($folder . '/bar'));
}
public function testRemoveRecursive(): void {
$folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
$fileData = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'text/plain'];
$folders = ['folder', 'folder/subfolder', 'folder/sub2', 'folder/sub2/sub3'];
$files = ['folder/foo.txt', 'folder/bar.txt', 'folder/subfolder/asd.txt', 'folder/sub2/qwerty.txt', 'folder/sub2/sub3/foo.txt'];
foreach ($folders as $folder) {
$this->cache->put($folder, $folderData);
}
foreach ($files as $file) {
$this->cache->put($file, $fileData);
}
$this->cache->remove('folder');
foreach ($files as $file) {
$this->assertFalse($this->cache->inCache($file));
}
}
/**
* @return array{0: Cache, 1: \Closure(): list<CacheEntriesRemovedEvent>}
*/
private function cacheWithRecordedEvents(IStorage $storage): array {
$events = [];
$dispatcher = $this->createMock(IEventDispatcher::class);
$dispatcher->method('dispatchTyped')
->willReturnCallback(function (object $event) use (&$events): void {
if ($event instanceof CacheEntriesRemovedEvent) {
$events[] = $event;
}
});
$dependencies = new CacheDependencies(
Server::get(IMimeTypeLoader::class),
Server::get(IDBConnection::class),
$dispatcher,
Server::get(QuerySearchHelper::class),
Server::get(SystemConfig::class),
Server::get(LoggerInterface::class),
Server::get(IFilesMetadataManager::class),
Server::get(DisplayNameCache::class),
);
return [new Cache($storage, $dependencies), function () use (&$events): array {
return $events;
}];
}
/**
* @param list<CacheEntriesRemovedEvent> $events
* @return list<int>
*/
private function announcedFileIds(array $events): array {
$fileIds = [];
foreach ($events as $event) {
foreach ($event->getCacheEntryRemovedEvents() as $removed) {
$fileIds[] = $removed->getFileId();
}
}
return $fileIds;
}
public function testRemoveSingleFileAnnouncesBatchEvent(): void {
$storage = new Temporary([]);
[$cache, $recorded] = $this->cacheWithRecordedEvents($storage);
$cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]);
$fileId = $cache->put('foo.txt', ['size' => 1, 'mtime' => 20, 'mimetype' => 'text/plain']);
$cache->remove('foo.txt');
$this->assertEquals([$fileId], $this->announcedFileIds($recorded()));
}
public function testRemoveRecursiveAnnouncesRealFileIds(): void {
$storage = new Temporary([]);
[$cache, $recorded] = $this->cacheWithRecordedEvents($storage);
$cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]);
$folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
$fileData = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'text/plain'];
$expected = [$cache->put('folder', $folderData)];
$expected[] = $cache->put('folder/sub', $folderData);
$expected[] = $cache->put('folder/foo.txt', $fileData);
$expected[] = $cache->put('folder/sub/bar.txt', $fileData);
$cache->remove('folder');
$announced = $this->announcedFileIds($recorded());
sort($expected);
sort($announced);
$this->assertEquals($expected, $announced);
}
public function testRemoveRecursiveAnnouncesEveryChildExactlyOnce(): void {
$storage = new Temporary([]);
[$cache, $recorded] = $this->cacheWithRecordedEvents($storage);
$cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]);
$fileData = ['size' => 1, 'mtime' => 20, 'mimetype' => 'text/plain'];
$expected = [$cache->put('folder', ['size' => 0, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE])];
for ($i = 0; $i < IQueryBuilder::MAX_IN_PARAMETERS + 1; $i++) {
$expected[] = $cache->insert("folder/child$i.txt", $fileData);
}
$cache->remove('folder');
$announced = $this->announcedFileIds($recorded());
$this->assertCount(count($expected), $announced, 'every removed entry is announced exactly once');
sort($expected);
sort($announced);
$this->assertEquals($expected, $announced);
}
public function testClearRemovesExtendedAndMetadataEntries(): void {
$cache = new Cache($this->storage);
$fileId = $cache->put('foo', ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'upload_time' => 30]);
$metadataManager = Server::get(IFilesMetadataManager::class);
$metadata = $metadataManager->getMetadata($fileId, true);
$metadata->setString('test-key', 'value', true);
$metadataManager->saveMetadata($metadata);
$cache->clear();
$this->assertFalse($cache->inCache('foo'));
$connection = Server::get(IDBConnection::class);
foreach (['filecache_extended' => 'fileid', 'files_metadata' => 'file_id', 'files_metadata_index' => 'file_id'] as $table => $column) {
$query = $connection->getQueryBuilder();
$query->select($column)
->from($table)
->where($query->expr()->eq($column, $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
$this->assertSame([], $query->executeQuery()->fetchFirstColumn(), "$table rows of the cleared storage remain");
}
}
public static function folderDataProvider(): array {
return [
['folder'],
// that was too easy, try something harder
['☺, WHITE SMILING FACE, UTF-8 hex E298BA'],
// what about 4 byte utf-8
['😐, NEUTRAL_FACE, UTF-8 hex F09F9890'],
// now the crazy stuff
[', UNASSIGNED PRIVATE USE, UTF-8 hex EF9890'],
// and my favorite
['w͢͢͝h͡o͢͡ ̸͢k̵͟n̴͘ǫw̸̛s͘ ̀́w͘͢ḩ̵a҉̡͢t ̧̕h́o̵r͏̵rors̡ ̶͡͠lį̶e͟͟ ̶͝in͢ ͏t̕h̷̡͟e ͟͟d̛a͜r̕͡k̢̨ ͡h̴e͏a̷̢̡rt́͏ ̴̷͠ò̵̶f̸ u̧͘ní̛͜c͢͏o̷͏d̸͢e̡͝']
];
}
public function testEncryptedFolder(): void {
$file1 = 'folder';
$file2 = 'folder/bar';
$file3 = 'folder/foo';
$data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
$fileData = [];
$fileData['bar'] = ['size' => 1000, 'encrypted' => 1, 'mtime' => 20, 'mimetype' => 'foo/file'];
$fileData['foo'] = ['size' => 20, 'encrypted' => 1, 'mtime' => 25, 'mimetype' => 'foo/file'];
$this->cache->put($file1, $data1);
$this->cache->put($file2, $fileData['bar']);
$this->cache->put($file3, $fileData['foo']);
$content = $this->cache->getFolderContents($file1);
$this->assertEquals(count($content), 2);
foreach ($content as $cachedData) {
$data = $fileData[$cachedData['name']];
}
$file4 = 'folder/unkownSize';
$fileData['unkownSize'] = ['size' => -1, 'mtime' => 25, 'mimetype' => 'foo/file'];
$this->cache->put($file4, $fileData['unkownSize']);
$this->assertEquals(-1, $this->cache->calculateFolderSize($file1));
$fileData['unkownSize'] = ['size' => 5, 'mtime' => 25, 'mimetype' => 'foo/file'];
$this->cache->put($file4, $fileData['unkownSize']);
$this->assertEquals(1025, $this->cache->calculateFolderSize($file1));
// direct cache entry retrieval returns the original values
$entry = $this->cache->get($file1);
$this->assertEquals(1025, $entry['size']);
$this->cache->remove($file2);
$this->cache->remove($file3);
$this->cache->remove($file4);
$this->assertEquals(0, $this->cache->calculateFolderSize($file1));
$this->cache->remove('folder');
$this->assertFalse($this->cache->inCache('folder/foo'));
$this->assertFalse($this->cache->inCache('folder/bar'));
}
public function testCalculateFolderSizeWithEncryptedZeroByte(): void {
$folder = 'enc_folder';
$this->cache->put($folder, ['size' => -1, 'mtime' => 20, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]);
// Child 1: zero-byte encrypted file — on-disk 8192 (header only), plaintext 0
$child1 = $folder . '/empty.enc';
$this->cache->put($child1, [
'size' => 8192,
'mtime' => 20,
'mimetype' => 'application/octet-stream',
'encrypted' => 1,
'unencrypted_size' => 0,
]);
// Child 2: non-zero encrypted file — opens the write-back gate ($unencryptedMax > 0)
$child2 = $folder . '/small.enc';
$this->cache->put($child2, [
'size' => 8292,
'mtime' => 20,
'mimetype' => 'application/octet-stream',
'encrypted' => 1,
'unencrypted_size' => 100,
]);
$this->cache->calculateFolderSize($folder);
$entry = $this->cache->get($folder);
// Must sum plaintext sizes (0 + 100 = 100), not fall back to on-disk size for
// the zero-byte child (8192 + 100 = 8292 with the old buggy code)
$this->assertEquals(100, $entry['unencrypted_size'], 'Folder unencrypted_size should sum plaintext sizes');
$this->assertEquals(16484, $entry['size'], 'Folder size should sum on-disk sizes (8192 + 8292)');
}
public function testRootFolderSizeForNonHomeStorage(): void {
$dir1 = 'knownsize';
$dir2 = 'unknownsize';
$fileData = [];
$fileData[''] = ['size' => -1, 'mtime' => 20, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
$fileData[$dir1] = ['size' => 1000, 'mtime' => 20, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
$fileData[$dir2] = ['size' => -1, 'mtime' => 25, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
$this->cache->put($dir1, $fileData[$dir1]);
$this->cache->put($dir2, $fileData[$dir2]);
$this->assertTrue($this->cache->inCache($dir1));
$this->assertTrue($this->cache->inCache($dir2));
// check that root size ignored the unknown sizes
$this->assertEquals(-1, $this->cache->calculateFolderSize(''));
// clean up
$this->cache->remove('');
$this->cache->remove($dir1);
$this->cache->remove($dir2);
$this->assertFalse($this->cache->inCache($dir1));
$this->assertFalse($this->cache->inCache($dir2));
}
public function testStatus(): void {
$this->assertEquals(Cache::NOT_FOUND, $this->cache->getStatus('foo'));
$this->cache->put('foo', ['size' => -1]);
$this->assertEquals(Cache::PARTIAL, $this->cache->getStatus('foo'));
$this->cache->put('foo', ['size' => -1, 'mtime' => 20, 'mimetype' => 'foo/file']);
$this->assertEquals(Cache::SHALLOW, $this->cache->getStatus('foo'));
$this->cache->put('foo', ['size' => 10]);
$this->assertEquals(Cache::COMPLETE, $this->cache->getStatus('foo'));
}
public static function putWithAllKindOfQuotesData(): array {
return [
['`backtick`'],
['´forward´'],
['\'single\''],
];
}
/**
* @param $fileName
*/
#[\PHPUnit\Framework\Attributes\DataProvider('putWithAllKindOfQuotesData')]
public function testPutWithAllKindOfQuotes($fileName): void {
$this->assertEquals(Cache::NOT_FOUND, $this->cache->get($fileName));
$this->cache->put($fileName, ['size' => 20, 'mtime' => 25, 'mimetype' => 'foo/file', 'etag' => $fileName]);
$cacheEntry = $this->cache->get($fileName);
$this->assertEquals($fileName, $cacheEntry['etag']);
$this->assertEquals($fileName, $cacheEntry['path']);
}
public function testSearch(): void {
$file1 = 'folder';
$file2 = 'folder/foobar';
$file3 = 'folder/foo';
$data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder'];
$fileData = [];
$fileData['foobar'] = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file'];
$fileData['foo'] = ['size' => 20, 'mtime' => 25, 'mimetype' => 'foo/file'];
$this->cache->put($file1, $data1);
$this->cache->put($file2, $fileData['foobar']);
$this->cache->put($file3, $fileData['foo']);
$this->assertEquals(2, count($this->cache->search('%foo%')));
$this->assertEquals(1, count($this->cache->search('foo')));
$this->assertEquals(1, count($this->cache->search('%folder%')));
$this->assertEquals(1, count($this->cache->search('folder%')));
// case insensitive search should match the same files
$this->assertEquals(2, count($this->cache->search('%Foo%')));
$this->assertEquals(1, count($this->cache->search('Foo')));
$this->assertEquals(1, count($this->cache->search('%Folder%')));
$this->assertEquals(1, count($this->cache->search('Folder%')));
$this->assertEquals(3, count($this->cache->searchByMime('foo')));
$this->assertEquals(2, count($this->cache->searchByMime('foo/file')));
}
public function testSearchQueryByTag(): void {
$userId = static::getUniqueID('user');
Server::get(IUserManager::class)->createUser($userId, $userId);
static::loginAsUser($userId);
$user = new User($userId, null, Server::get(IEventDispatcher::class));
$file1 = 'folder';
$file2 = 'folder/foobar';
$file3 = 'folder/foo';
$file4 = 'folder/foo2';
$file5 = 'folder/foo3';
$data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder'];
$fileData = [];
$fileData['foobar'] = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file'];
$fileData['foo'] = ['size' => 20, 'mtime' => 25, 'mimetype' => 'foo/file'];
$fileData['foo2'] = ['size' => 25, 'mtime' => 28, 'mimetype' => 'foo/file'];
$fileData['foo3'] = ['size' => 88, 'mtime' => 34, 'mimetype' => 'foo/file'];
$id1 = $this->cache->put($file1, $data1);
$id2 = $this->cache->put($file2, $fileData['foobar']);
$id3 = $this->cache->put($file3, $fileData['foo']);
$id4 = $this->cache->put($file4, $fileData['foo2']);
$id5 = $this->cache->put($file5, $fileData['foo3']);
$tagManager = Server::get(ITagManager::class)->load('files', [], false, $userId);
$this->assertTrue($tagManager->tagAs($id1, 'tag1'));
$this->assertTrue($tagManager->tagAs($id1, 'tag2'));
$this->assertTrue($tagManager->tagAs($id2, 'tag2'));
$this->assertTrue($tagManager->tagAs($id3, 'tag1'));
$this->assertTrue($tagManager->tagAs($id4, 'tag2'));
$results = $this->cache->searchQuery(new SearchQuery(
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'tagname', 'tag2'),
0, 0, [], $user
));
$this->assertEquals(3, count($results));
usort($results, function ($value1, $value2) {
return $value1['name'] <=> $value2['name'];
});
$this->assertEquals('folder', $results[0]['name']);
$this->assertEquals('foo2', $results[1]['name']);
$this->assertEquals('foobar', $results[2]['name']);
$tagManager->delete('tag1');
$tagManager->delete('tag2');
static::logout();
$user = Server::get(IUserManager::class)->get($userId);
if ($user !== null) {
try {
$user->delete();
} catch (\Exception $e) {
}
}
}
public function testSearchByQuery(): void {
$file1 = 'folder';
$file2 = 'folder/foobar';
$file3 = 'folder/foo';
$data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder'];
$fileData = [];
$fileData['foobar'] = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file'];
$fileData['foo'] = ['size' => 20, 'mtime' => 25, 'mimetype' => 'foo/file'];
$this->cache->put($file1, $data1);
$this->cache->put($file2, $fileData['foobar']);
$this->cache->put($file3, $fileData['foo']);
/** @var IUser $user */
$user = $this->createMock(IUser::class);
$this->assertCount(1, $this->cache->searchQuery(new SearchQuery(
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foo'), 10, 0, [], $user)));
$this->assertCount(2, $this->cache->searchQuery(new SearchQuery(
new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', 'foo%'), 10, 0, [], $user)));
$this->assertCount(2, $this->cache->searchQuery(new SearchQuery(
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'foo/file'), 10, 0, [], $user)));
$this->assertCount(3, $this->cache->searchQuery(new SearchQuery(
new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'foo/%'), 10, 0, [], $user)));
$this->assertCount(1, $this->cache->searchQuery(new SearchQuery(
new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'size', 100), 10, 0, [], $user)));
$this->assertCount(2, $this->cache->searchQuery(new SearchQuery(
new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'size', 100), 10, 0, [], $user)));
}
public static function movePathProvider(): array {
return [
['folder/foo', 'folder/foobar', ['1', '2']],
['folder/foo', 'foo', ['1', '2']],
['files/Индустрия_Инженерные системы ЦОД', 'files/Индустрия_Инженерные системы ЦОД1', ['1', '2']],
];
}
#[\PHPUnit\Framework\Attributes\DataProvider('movePathProvider')]
public function testMove($sourceFolder, $targetFolder, $children): void {
$data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/bar'];
$folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
// create folders
foreach ([$sourceFolder, $targetFolder] as $current) {
while (strpos($current, '/') > 0) {
$current = dirname($current);
$this->cache->put($current, $folderData);
$this->cache2->put($current, $folderData);
}
}
$this->cache->put($sourceFolder, $folderData);
$this->cache2->put($sourceFolder, $folderData);
foreach ($children as $child) {
$this->cache->put($sourceFolder . '/' . $child, $data);
$this->cache2->put($sourceFolder . '/' . $child, $data);
}
$this->cache->move($sourceFolder, $targetFolder);
$this->assertFalse($this->cache->inCache($sourceFolder));
$this->assertTrue($this->cache2->inCache($sourceFolder));
$this->assertTrue($this->cache->inCache($targetFolder));
$this->assertFalse($this->cache2->inCache($targetFolder));
foreach ($children as $child) {
$this->assertFalse($this->cache->inCache($sourceFolder . '/' . $child));
$this->assertTrue($this->cache2->inCache($sourceFolder . '/' . $child));
$this->assertTrue($this->cache->inCache($targetFolder . '/' . $child));
$this->assertFalse($this->cache2->inCache($targetFolder . '/' . $child));
}
}
public function testMoveFromCache(): void {
$data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/bar'];
$folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
$this->cache2->put('folder', $folderData);
$this->cache2->put('folder/sub', $data);
$this->cache->moveFromCache($this->cache2, 'folder', 'targetfolder');
$this->assertFalse($this->cache2->inCache('folder'));
$this->assertFalse($this->cache2->inCache('folder/sub'));
$this->assertTrue($this->cache->inCache('targetfolder'));
$this->assertTrue($this->cache->inCache('targetfolder/sub'));
}
public function testMoveFromCacheJail(): void {
$data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/bar'];
$folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
$this->cache2->put('folder', $folderData);
$this->cache2->put('folder/sub', $data);
$jail = new CacheJail($this->cache2, 'folder');
$this->cache->moveFromCache($jail, 'sub', 'targetsub');
$this->assertTrue($this->cache2->inCache('folder'));
$this->assertFalse($this->cache2->inCache('folder/sub'));
$this->assertTrue($this->cache->inCache('targetsub'));
$this->assertEquals($this->cache->getId(''), $this->cache->get('targetsub')->getParentId());
}
public function testCopyFromCachePreservesEncryptedVersion(): void {
$data = [
'size' => 100, 'mtime' => 50, 'mimetype' => 'foo/bar',
'encrypted' => true, 'encryptedVersion' => 3,
];
$this->cache->put('source', $data);
$sourceEntry = $this->cache->get('source');
$this->assertSame(3, $sourceEntry['encryptedVersion']);
$this->cache->copyFromCache($this->cache, $sourceEntry, 'target');
$targetEntry = $this->cache->get('target');
$this->assertTrue($targetEntry->isEncrypted());
$this->assertSame(3, $targetEntry['encryptedVersion']);
}
public function testCopyFromCachePreservesUnencryptedSize(): void {
$data = [
'size' => 128, 'mtime' => 50, 'mimetype' => 'foo/bar',
'encrypted' => true, 'encryptedVersion' => 3, 'unencrypted_size' => 100,
];
$this->cache->put('source', $data);
$sourceEntry = $this->cache->get('source');
$this->assertEquals(100, $sourceEntry->getUnencryptedSize());
$this->cache->copyFromCache($this->cache, $sourceEntry, 'target');
$targetEntry = $this->cache->get('target');
$this->assertEquals(100, $targetEntry->getUnencryptedSize());
}
public function testCopyFromCacheClearsEncryptedVersionWhenCopyingToNonEncryptedStorage(): void {
$data = [
'size' => 128, 'mtime' => 50, 'mimetype' => 'foo/bar',
'encrypted' => true, 'encryptedVersion' => 3, 'unencrypted_size' => 100,
];
$this->cache2->put('source', $data);
$sourceEntry = $this->cache2->get('source');
$sourceCache = $this->getMockBuilder(Cache::class)
->setConstructorArgs([$this->storage2])
->onlyMethods(['hasEncryptionWrapper'])
->getMock();
$sourceCache->method('hasEncryptionWrapper')->willReturn(true);
$targetCache = $this->getMockBuilder(Cache::class)
->setConstructorArgs([$this->storage])
->onlyMethods(['shouldEncrypt'])
->getMock();
$targetCache->method('shouldEncrypt')->willReturn(false);
$targetCache->copyFromCache($sourceCache, $sourceEntry, 'target');
$targetEntry = $targetCache->get('target');
$this->assertFalse($targetEntry->isEncrypted());
$this->assertSame(0, $targetEntry['encryptedVersion']);
// the target is not marked as encrypted, so its size is read from `size`
$this->assertEquals(0, $targetEntry['unencrypted_size']);
$this->assertEquals(128, $targetEntry->getSize());
}
public function testGetIncomplete(): void {
$file1 = 'folder1';
$file2 = 'folder2';
$file3 = 'folder3';
$file4 = 'folder4';
$data = ['size' => 10, 'mtime' => 50, 'mimetype' => 'foo/bar'];
$this->cache->put($file1, $data);
$data['size'] = -1;
$this->cache->put($file2, $data);
$this->cache->put($file3, $data);
$data['size'] = 12;
$this->cache->put($file4, $data);
$this->assertEquals($file3, $this->cache->getIncomplete());
}
public function testNonExisting(): void {
$this->assertFalse($this->cache->get('foo.txt'));
$this->assertFalse($this->cache->get(-1));
$this->assertEquals([], $this->cache->getFolderContents('foo'));
}
public function testGetById(): void {
$storageId = $this->storage->getId();
$data = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file'];
$id = $this->cache->put('foo', $data);
if (strlen($storageId) > 64) {
$storageId = md5($storageId);
}
$this->assertEquals([$storageId, 'foo'], Cache::getById($id));
}
public function testStorageMTime(): void {
$data = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file'];
$this->cache->put('foo', $data);
$cachedData = $this->cache->get('foo');
$this->assertEquals($data['mtime'], $cachedData['storage_mtime']); //if no storage_mtime is saved, mtime should be used
$this->cache->put('foo', ['storage_mtime' => 30]); //when setting storage_mtime, mtime is also set
$cachedData = $this->cache->get('foo');
$this->assertEquals(30, $cachedData['storage_mtime']);
$this->assertEquals(30, $cachedData['mtime']);
$this->cache->put('foo', ['mtime' => 25]); //setting mtime does not change storage_mtime
$cachedData = $this->cache->get('foo');
$this->assertEquals(30, $cachedData['storage_mtime']);
$this->assertEquals(25, $cachedData['mtime']);
}
public function testLongId(): void {
$storage = new LongId([]);
$cache = $storage->getCache();
$cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]);
$storageId = $storage->getId();
$data = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file'];
$id = $cache->put('foo', $data);
$this->assertEquals([md5($storageId), 'foo'], Cache::getById($id));
}
/**
* this test show the bug resulting if we have no normalizer installed
*/
public function testWithoutNormalizer(): void {
// folder name "Schön" with U+00F6 (normalized)
$folderWith00F6 = "\x53\x63\x68\xc3\xb6\x6e";
// folder name "Schön" with U+0308 (un-normalized)
$folderWith0308 = "\x53\x63\x68\x6f\xcc\x88\x6e";
/**
* @var Cache|\PHPUnit\Framework\MockObject\MockObject $cacheMock
*/
$cacheMock = $this->getMockBuilder(Cache::class)
->onlyMethods(['normalize'])
->setConstructorArgs([$this->storage])
->getMock();
$cacheMock->expects($this->any())
->method('normalize')
->willReturnArgument(0);
$data = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
// put root folder
$this->assertFalse($cacheMock->get('folder'));
$this->assertGreaterThan(0, $cacheMock->put('folder', $data));
// put un-normalized folder
$this->assertFalse($cacheMock->get('folder/' . $folderWith0308));
$this->assertGreaterThan(0, $cacheMock->put('folder/' . $folderWith0308, $data));
// get un-normalized folder by name
$unNormalizedFolderName = $cacheMock->get('folder/' . $folderWith0308);
// check if database layer normalized the folder name (this should not happen)
$this->assertEquals($folderWith0308, $unNormalizedFolderName['name']);
// put normalized folder
$this->assertFalse($cacheMock->get('folder/' . $folderWith00F6));
$this->assertGreaterThan(0, $cacheMock->put('folder/' . $folderWith00F6, $data));
// this is our bug, we have two different hashes with the same name (Schön)
$this->assertEquals(2, count($cacheMock->getFolderContents('folder')));
}
/**
* this test shows that there is no bug if we use the normalizer
*/
public function testWithNormalizer(): void {
if (!class_exists('Patchwork\PHP\Shim\Normalizer')) {
$this->markTestSkipped('The 3rdparty Normalizer extension is not available.');
return;
}
// folder name "Schön" with U+00F6 (normalized)
$folderWith00F6 = "\x53\x63\x68\xc3\xb6\x6e";
// folder name "Schön" with U+0308 (un-normalized)
$folderWith0308 = "\x53\x63\x68\x6f\xcc\x88\x6e";
$data = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
// put root folder
$this->assertFalse($this->cache->get('folder'));
$this->assertGreaterThan(0, $this->cache->put('folder', $data));
// put un-normalized folder
$this->assertFalse($this->cache->get('folder/' . $folderWith0308));
$this->assertGreaterThan(0, $this->cache->put('folder/' . $folderWith0308, $data));
// get un-normalized folder by name
$unNormalizedFolderName = $this->cache->get('folder/' . $folderWith0308);
// check if folder name was normalized
$this->assertEquals($folderWith00F6, $unNormalizedFolderName['name']);
// put normalized folder
$this->assertInstanceOf('\OCP\Files\Cache\ICacheEntry', $this->cache->get('folder/' . $folderWith00F6));
$this->assertGreaterThan(0, $this->cache->put('folder/' . $folderWith00F6, $data));
// at this point we should have only one folder named "Schön"
$this->assertEquals(1, count($this->cache->getFolderContents('folder')));
}
public static function bogusPathNamesProvider(): array {
return [
['/bogus.txt', 'bogus.txt'],
['//bogus.txt', 'bogus.txt'],
['bogus/', 'bogus'],
['bogus//', 'bogus'],
];
}
/**
* Test bogus paths with leading or doubled slashes
*/
#[\PHPUnit\Framework\Attributes\DataProvider('bogusPathNamesProvider')]
public function testBogusPaths($bogusPath, $fixedBogusPath): void {
$data = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
$parentId = $this->cache->getId('');
$this->assertGreaterThan(0, $this->cache->put($bogusPath, $data));
$newData = $this->cache->get($fixedBogusPath);
$this->assertNotFalse($newData);
$this->assertEquals($fixedBogusPath, $newData['path']);
// parent is the correct one, resolved properly (they used to not be)
$this->assertEquals($parentId, $newData['parent']);
$newDataFromBogus = $this->cache->get($bogusPath);
// same entry
$this->assertEquals($newData, $newDataFromBogus);
}
public function testNoReuseOfFileId(): void {
$data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain'];
$this->cache->put('somefile.txt', $data1);
$info = $this->cache->get('somefile.txt');
$fileId = $info['fileid'];
$this->cache->remove('somefile.txt');
$data2 = ['size' => 200, 'mtime' => 100, 'mimetype' => 'text/plain'];
$this->cache->put('anotherfile.txt', $data2);
$info2 = $this->cache->get('anotherfile.txt');
$fileId2 = $info2['fileid'];
$this->assertNotEquals($fileId, $fileId2);
}
public static function escapingProvider(): array {
return [
['foo'],
['o%'],
['oth_r'],
];
}
/**
* @param string $name
*/
#[\PHPUnit\Framework\Attributes\DataProvider('escapingProvider')]
public function testEscaping($name): void {
$data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain'];
$this->cache->put($name, $data);
$this->assertTrue($this->cache->inCache($name));
$retrievedData = $this->cache->get($name);
foreach ($data as $key => $value) {
$this->assertEquals($value, $retrievedData[$key]);
}
$this->cache->move($name, $name . 'asd');
$this->assertFalse($this->cache->inCache($name));
$this->assertTrue($this->cache->inCache($name . 'asd'));
$this->cache->remove($name . 'asd');
$this->assertFalse($this->cache->inCache($name . 'asd'));
$folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
$this->cache->put($name, $folderData);
$this->cache->put('other', $folderData);
$childs = ['asd', 'bar', 'foo', 'sub/folder'];
$this->cache->put($name . '/sub', $folderData);
$this->cache->put('other/sub', $folderData);
foreach ($childs as $child) {
$this->cache->put($name . '/' . $child, $data);
$this->cache->put('other/' . $child, $data);
$this->assertTrue($this->cache->inCache($name . '/' . $child));
}
$this->cache->move($name, $name . 'asd');
foreach ($childs as $child) {
$this->assertTrue($this->cache->inCache($name . 'asd/' . $child));
$this->assertTrue($this->cache->inCache('other/' . $child));
}
foreach ($childs as $child) {
$this->cache->remove($name . 'asd/' . $child);
$this->assertFalse($this->cache->inCache($name . 'asd/' . $child));
$this->assertTrue($this->cache->inCache('other/' . $child));
}
}
public function testExtended(): void {
$folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE];
$data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'creation_time' => 20];
$id1 = $this->cache->put('foo1', $data);
$data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'upload_time' => 30];
$this->cache->put('foo2', $data);
$data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'metadata_etag' => 'foo'];
$this->cache->put('foo3', $data);
$data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain'];
$id4 = $this->cache->put('foo4', $data);
$entry = $this->cache->get($id1);
$this->assertEquals(20, $entry->getCreationTime());
$this->assertEquals(0, $entry->getUploadTime());
$this->assertEquals(null, $entry->getMetadataEtag());
$entries = $this->cache->getFolderContents('');
$this->assertCount(4, $entries);
$entriesByName = [];
foreach ($entries as $entry) {
$entriesByName[$entry->getName()] = $entry;
}
$this->assertArrayHasKey('foo1', $entriesByName);
$this->assertArrayHasKey('foo2', $entriesByName);
$this->assertArrayHasKey('foo3', $entriesByName);
$this->assertArrayHasKey('foo4', $entriesByName);
$this->assertEquals(20, $entriesByName['foo1']->getCreationTime());
$this->assertEquals(0, $entriesByName['foo1']->getUploadTime());
$this->assertEquals(null, $entriesByName['foo1']->getMetadataEtag());
$this->assertEquals(0, $entriesByName['foo2']->getCreationTime());
$this->assertEquals(30, $entriesByName['foo2']->getUploadTime());
$this->assertEquals(null, $entriesByName['foo2']->getMetadataEtag());
$this->assertEquals(0, $entriesByName['foo3']->getCreationTime());
$this->assertEquals(0, $entriesByName['foo3']->getUploadTime());
$this->assertEquals('foo', $entriesByName['foo3']->getMetadataEtag());
$this->assertEquals(0, $entriesByName['foo4']->getCreationTime());
$this->assertEquals(0, $entriesByName['foo4']->getUploadTime());
$this->assertEquals(null, $entriesByName['foo4']->getMetadataEtag());
$this->cache->update($id1, ['upload_time' => 25]);
$entry = $this->cache->get($id1);
$this->assertEquals(20, $entry->getCreationTime());
$this->assertEquals(25, $entry->getUploadTime());
$this->assertEquals(null, $entry->getMetadataEtag());
$this->cache->put('sub', $folderData);
$this->cache->move('foo1', 'sub/foo1');
$entries = $this->cache->getFolderContents('sub');
$this->assertCount(1, $entries);
$this->assertEquals('foo1', $entries[0]->getName());
$this->assertEquals(20, $entries[0]->getCreationTime());
$this->assertEquals(25, $entries[0]->getUploadTime());
$this->assertEquals(null, $entries[0]->getMetadataEtag());
$this->cache->update($id4, ['upload_time' => 25]);
$entry = $this->cache->get($id4);
$this->assertEquals(0, $entry->getCreationTime());
$this->assertEquals(25, $entry->getUploadTime());
$this->assertEquals(null, $entry->getMetadataEtag());
$this->cache->remove('sub');
}
}