Files
Stephen Cuppett ecfcd152b1 test(encryption): strengthen testCalculateFolderSizeWithEncryptedZeroByte
test(encryption): strengthen testCalculateFolderSizeWithEncryptedZeroByte

Add a second encrypted child with unencrypted_size=100 so the write-back

gate in calculateFolderSizeInner ($unencryptedMax > 0) actually opens.

The original single zero-byte child left the gate closed, meaning the

test passed against the DB default rather than the computed value.

With two children the assertion distinguishes the fixed code (result: 100)

from the old buggy code (result: 8292, falling back to on-disk size for

the zero-byte child).

Assisted-by: ClaudeCode:claude-opus-4-6

Signed-off-by: Stephen Cuppett <scuppett@redhat.com>

[skip ci]
2026-08-03 09:28:12 +00:00

942 lines
34 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\CacheEntry;
use OC\Files\Cache\Wrapper\CacheJail;
use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchQuery;
use OC\Files\Storage\Temporary;
use OC\User\User;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Search\ISearchComparison;
use OCP\IDBConnection;
use OCP\ITagManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Server;
class LongId extends Temporary {
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;
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]);
}
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));
}
}
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 testCopyFromCacheClearsEncryptedVersionWhenCopyingToNonEncryptedStorage(): void {
$data = [
'size' => 100, 'mtime' => 50, 'mimetype' => 'foo/bar',
'encrypted' => true, 'encryptedVersion' => 3,
];
$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']);
}
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');
}
}