From 57df4d8629a3184cfbbd1a8888805e0566eb9cdd Mon Sep 17 00:00:00 2001 From: Kent Delante Date: Thu, 16 Jul 2026 16:38:24 +0800 Subject: [PATCH 1/2] fix(files): preserve encryptedVersion when copying cache entries Cache::copyFromCache() rebuilt the target's cache row via cacheEntryToArray(), which only carried over a boolean `encrypted` flag and dropped the real `encryptedVersion` count. Since View::copy() unconditionally calls copyFromCache() after the storage copy completes, this silently overwrote the correct encryptedVersion that Encryption::updateEncryptedVersion() had just set, collapsing it back to 0/1 on every copy of a file whose encryptedVersion was above 1. cacheEntryToArray() now includes the source entry's encryptedVersion when it is encrypted. The existing encrypted-to-non-encrypted-storage override in copyFromCache() also clears encryptedVersion alongside `encrypted`, since normalizeData() prefers encryptedVersion over `encrypted` when both are present in the update data. Signed-off-by: Kent Delante Assisted-by: ClaudeCode:claude-sonnet-5 --- lib/private/Files/Cache/Cache.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index ae5bbe011c9d6..67156893c3fd2 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -1166,6 +1166,9 @@ public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, str // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark if ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) { $data['encrypted'] = 0; + // normalizeData() prefers 'encryptedVersion' over 'encrypted' when both are + // set, so it has to be cleared too or the mark above gets ignored + unset($data['encryptedVersion']); } $fileId = $this->put($targetPath, $data); @@ -1199,6 +1202,11 @@ private function cacheEntryToArray(ICacheEntry $entry): array { if ($entry instanceof CacheEntry && isset($entry['scan_permissions'])) { $data['permissions'] = $entry['scan_permissions']; } + + if ($entry->isEncrypted() && isset($entry['encryptedVersion'])) { + $data['encryptedVersion'] = $entry['encryptedVersion']; + } + return $data; } From eb4df90489ea7fbf36ca02ea5c6efb479c01cbd2 Mon Sep 17 00:00:00 2001 From: Kent Delante Date: Thu, 16 Jul 2026 16:54:47 +0800 Subject: [PATCH 2/2] test: add tests for encryptedVersion cache entry preservation Signed-off-by: Kent Delante Assisted-by: ClaudeCode:claude-sonnet-5 --- tests/lib/Files/Cache/CacheTest.php | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/lib/Files/Cache/CacheTest.php b/tests/lib/Files/Cache/CacheTest.php index 821b371294af7..6c9f67f07a0f4 100644 --- a/tests/lib/Files/Cache/CacheTest.php +++ b/tests/lib/Files/Cache/CacheTest.php @@ -500,6 +500,49 @@ public function testMoveFromCache() { $this->assertTrue($this->cache->inCache('targetfolder/sub')); } + 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(['hasEncryptionWrapper']) + ->getMock(); + $targetCache->method('hasEncryptionWrapper')->willReturn(false); + + $targetCache->copyFromCache($sourceCache, $sourceEntry, 'target'); + + $targetEntry = $targetCache->get('target'); + $this->assertFalse($targetEntry['encrypted']); + $this->assertSame(0, $targetEntry['encryptedVersion']); + } + public function testGetIncomplete() { $file1 = 'folder1'; $file2 = 'folder2';