Skip to content

Commit 1b54b81

Browse files
Merge pull request #62780 from nextcloud/backport/60070/stable34
[stable34] fix(encryption): Correctly report size for zero-byte encrypted files
2 parents 0a6be1d + 655b0af commit 1b54b81

9 files changed

Lines changed: 88 additions & 11 deletions

File tree

lib/private/Files/Cache/Cache.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,7 +1043,7 @@ protected function calculateFolderSizeInner(string $path, $entry = null, bool $i
10431043
$id = $entry['fileid'];
10441044

10451045
$query = $this->getQueryBuilder();
1046-
$query->select('size', 'unencrypted_size')
1046+
$query->select('size', 'unencrypted_size', 'encrypted')
10471047
->from('filecache')
10481048
->whereStorageId($this->getNumericStorageId())
10491049
->whereParent($id);
@@ -1063,7 +1063,7 @@ protected function calculateFolderSizeInner(string $path, $entry = null, bool $i
10631063
return Util::numericToNumber($row['unencrypted_size']);
10641064
}, $rows);
10651065
$unencryptedSizes = array_map(function (array $row) {
1066-
return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
1066+
return Util::numericToNumber($row['encrypted'] ? $row['unencrypted_size'] : $row['size']);
10671067
}, $rows);
10681068

10691069
$sum = array_sum($sizes);

lib/private/Files/Cache/CacheEntry.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public function __clone() {
140140

141141
#[\Override]
142142
public function getUnencryptedSize(): int {
143-
if ($this->data['encrypted'] && isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) {
143+
if ($this->data['encrypted'] && isset($this->data['unencrypted_size'])) {
144144
return $this->data['unencrypted_size'];
145145
} else {
146146
return $this->data['size'] ?? 0;

lib/private/Files/Cache/Scanner.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,10 @@ public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData =
183183
}
184184
}
185185

186-
// we only updated unencrypted_size if it's already set
187-
if (isset($cacheData['unencrypted_size']) && $cacheData['unencrypted_size'] === 0) {
186+
// Skip updating unencrypted_size only when both cached and new values are 0
187+
if (isset($cacheData['unencrypted_size'])
188+
&& $cacheData['unencrypted_size'] === 0
189+
&& isset($data['unencrypted_size']) && $data['unencrypted_size'] === 0) {
188190
unset($data['unencrypted_size']);
189191
}
190192

@@ -202,7 +204,10 @@ public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData =
202204
$data['etag_changed'] = true;
203205
}
204206
} else {
205-
unset($data['unencrypted_size']);
207+
// For new files, only preserve unencrypted_size when the file is encrypted
208+
if (!isset($data['encrypted']) || !$data['encrypted']) {
209+
unset($data['unencrypted_size']);
210+
}
206211
$newData = $data;
207212
$fileId = -1;
208213
}

lib/private/Files/FileInfo.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public function __construct(
5858
private ?IUser $owner = null,
5959
) {
6060
$this->mount = $mount;
61-
if (isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] !== 0) {
61+
if (($this->data['encrypted'] ?? false) && isset($this->data['unencrypted_size'])) {
6262
$this->rawSize = $this->data['unencrypted_size'];
6363
} else {
6464
$this->rawSize = $this->data['size'] ?? 0;
@@ -173,7 +173,7 @@ public function getSize($includeMounts = true) {
173173
if ($includeMounts) {
174174
$this->updateEntryFromSubMounts();
175175

176-
if ($this->isEncrypted() && isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) {
176+
if ($this->isEncrypted() && isset($this->data['unencrypted_size'])) {
177177
return $this->data['unencrypted_size'];
178178
} else {
179179
return isset($this->data['size']) ? 0 + $this->data['size'] : 0;
@@ -355,7 +355,7 @@ public function addSubEntry($data, $entryPath) {
355355
if (!$data) {
356356
return;
357357
}
358-
$hasUnencryptedSize = isset($data['unencrypted_size']) && $data['unencrypted_size'] > 0;
358+
$hasUnencryptedSize = !empty($data['encrypted']) && isset($data['unencrypted_size']);
359359
if ($hasUnencryptedSize) {
360360
$subSize = $data['unencrypted_size'];
361361
} else {

lib/private/Files/Storage/Wrapper/Encryption.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ protected function verifyUnencryptedSize(string $path, int $unencryptedSize): in
400400
if ($unencryptedSize < 0
401401
|| ($size > 0 && $unencryptedSize === $size)
402402
|| $unencryptedSize > $size
403+
|| ($unencryptedSize === 0 && $size > $this->util->getHeaderSize())
403404
) {
404405
// check if we already calculate the unencrypted size for the
405406
// given path to avoid recursions

lib/private/Files/Stream/Encryption.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class Encryption extends Wrapper {
2828
protected string $cache;
2929
protected ?int $size = null;
3030
protected int $position;
31-
protected ?int $unencryptedSize = null;
31+
protected int|float|null $unencryptedSize = null;
3232
protected int $headerSize;
3333
protected int $unencryptedBlockSize;
3434
protected array $header;

tests/lib/Files/Cache/CacheTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,16 @@ public function testCacheEntryGetters(): void {
153153
$this->assertEquals($entry->getUnencryptedSize(), 100);
154154
}
155155

156+
public function testGetUnencryptedSizeEncryptedZeroByte(): void {
157+
$file1 = 'encrypted_zero';
158+
$this->cache->put($file1, ['size' => 8192, 'mtime' => 50, 'mimetype' => 'application/octet-stream', 'encrypted' => 1, 'unencrypted_size' => 0]);
159+
$entry = $this->cache->get($file1);
160+
161+
// getUnencryptedSize() must return 0 (the true plaintext size), not 8192 (the encrypted on-disk size)
162+
$this->assertEquals(0, $entry->getUnencryptedSize());
163+
$this->assertTrue($entry->isEncrypted());
164+
}
165+
156166
public function testPartial(): void {
157167
$file1 = 'foo';
158168

@@ -292,6 +302,39 @@ public function testEncryptedFolder(): void {
292302
$this->assertFalse($this->cache->inCache('folder/bar'));
293303
}
294304

305+
public function testCalculateFolderSizeWithEncryptedZeroByte(): void {
306+
$folder = 'enc_folder';
307+
$this->cache->put($folder, ['size' => -1, 'mtime' => 20, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]);
308+
309+
// Child 1: zero-byte encrypted file — on-disk 8192 (header only), plaintext 0
310+
$child1 = $folder . '/empty.enc';
311+
$this->cache->put($child1, [
312+
'size' => 8192,
313+
'mtime' => 20,
314+
'mimetype' => 'application/octet-stream',
315+
'encrypted' => 1,
316+
'unencrypted_size' => 0,
317+
]);
318+
319+
// Child 2: non-zero encrypted file — opens the write-back gate ($unencryptedMax > 0)
320+
$child2 = $folder . '/small.enc';
321+
$this->cache->put($child2, [
322+
'size' => 8292,
323+
'mtime' => 20,
324+
'mimetype' => 'application/octet-stream',
325+
'encrypted' => 1,
326+
'unencrypted_size' => 100,
327+
]);
328+
329+
$this->cache->calculateFolderSize($folder);
330+
331+
$entry = $this->cache->get($folder);
332+
// Must sum plaintext sizes (0 + 100 = 100), not fall back to on-disk size for
333+
// the zero-byte child (8192 + 100 = 8292 with the old buggy code)
334+
$this->assertEquals(100, $entry['unencrypted_size'], 'Folder unencrypted_size should sum plaintext sizes');
335+
$this->assertEquals(16484, $entry['size'], 'Folder size should sum on-disk sizes (8192 + 8292)');
336+
}
337+
295338
public function testRootFolderSizeForNonHomeStorage(): void {
296339
$dir1 = 'knownsize';
297340
$dir2 = 'unknownsize';

tests/lib/Files/FileInfoTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,30 @@ protected function setUp(): void {
2929
$this->config = $this->getMockBuilder(IConfig::class)->getMock();
3030
}
3131

32+
private function makeFileInfo(array $data): FileInfo {
33+
$storage = new Temporary();
34+
return new FileInfo('', $storage, '', $data, new MountPoint($storage, '/foo/files'));
35+
}
36+
37+
public function testGetSizeEncryptedZeroByte(): void {
38+
$info = $this->makeFileInfo(['encrypted' => true, 'size' => 8192, 'unencrypted_size' => 0]);
39+
// Both paths must report the true plaintext size (0), not the on-disk encrypted size (8192)
40+
$this->assertSame(0, $info->getSize(true));
41+
$this->assertSame(0, $info->getSize(false));
42+
}
43+
44+
public function testGetSizeEncryptedNonZero(): void {
45+
$info = $this->makeFileInfo(['encrypted' => true, 'size' => 16384, 'unencrypted_size' => 5000]);
46+
$this->assertSame(5000, $info->getSize(true));
47+
$this->assertSame(5000, $info->getSize(false));
48+
}
49+
50+
public function testGetSizeNonEncrypted(): void {
51+
$info = $this->makeFileInfo(['encrypted' => false, 'size' => 100]);
52+
$this->assertSame(100, $info->getSize(true));
53+
$this->assertSame(100, $info->getSize(false));
54+
}
55+
3256
public function testIsMountedHomeStorage(): void {
3357
$user = $this->createMock(IUser::class);
3458
$user->method('getUID')

tests/lib/Files/Storage/Wrapper/EncryptionTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,11 @@ public static function dataTestVerifyUnencryptedSize(): array {
387387
[120, 80, false, 80],
388388
[120, 120, false, 80],
389389
[120, -1, false, 80],
390-
[120, -1, true, -1]
390+
[120, -1, true, -1],
391+
// Zero-byte encrypted file: on-disk size equals header only (8192) — should NOT recalculate
392+
[8192, 0, false, 0],
393+
// Encrypted file with content beyond header but unencrypted_size=0 — SHOULD recalculate
394+
[16384, 0, false, 80],
391395
];
392396
}
393397

0 commit comments

Comments
 (0)