Merge pull request #60070 from cuppett/cuppett/fix-zero-byte-size

fix(encryption): Correctly report size for zero-byte encrypted files
pull/62405/merge
Stephen Cuppett 3 weeks ago committed by GitHub
commit 92035aeb62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      lib/private/Files/Cache/Cache.php
  2. 2
      lib/private/Files/Cache/CacheEntry.php
  3. 11
      lib/private/Files/Cache/Scanner.php
  4. 6
      lib/private/Files/FileInfo.php
  5. 1
      lib/private/Files/Storage/Wrapper/Encryption.php
  6. 2
      lib/private/Files/Stream/Encryption.php
  7. 43
      tests/lib/Files/Cache/CacheTest.php
  8. 24
      tests/lib/Files/FileInfoTest.php
  9. 6
      tests/lib/Files/Storage/Wrapper/EncryptionTest.php

@ -1042,7 +1042,7 @@ class Cache implements ICache {
$id = $entry['fileid'];
$query = $this->getQueryBuilder();
$query->select('size', 'unencrypted_size')
$query->select('size', 'unencrypted_size', 'encrypted')
->from('filecache')
->whereStorageId($this->getNumericStorageId())
->whereParent($id);
@ -1062,7 +1062,7 @@ class Cache implements ICache {
return Util::numericToNumber($row['unencrypted_size']);
}, $rows);
$unencryptedSizes = array_map(function (array $row) {
return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
return Util::numericToNumber($row['encrypted'] ? $row['unencrypted_size'] : $row['size']);
}, $rows);
$sum = array_sum($sizes);

@ -137,7 +137,7 @@ class CacheEntry implements ICacheEntry {
#[\Override]
public function getUnencryptedSize(): int {
if ($this->data['encrypted'] && isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) {
if ($this->data['encrypted'] && isset($this->data['unencrypted_size'])) {
return $this->data['unencrypted_size'];
} else {
return $this->data['size'] ?? 0;

@ -184,8 +184,10 @@ class Scanner extends BasicEmitter implements IScanner {
}
}
// we only updated unencrypted_size if it's already set
if (isset($cacheData['unencrypted_size']) && $cacheData['unencrypted_size'] === 0) {
// Skip updating unencrypted_size only when both cached and new values are 0
if (isset($cacheData['unencrypted_size'])
&& $cacheData['unencrypted_size'] === 0
&& isset($data['unencrypted_size']) && $data['unencrypted_size'] === 0) {
unset($data['unencrypted_size']);
}
@ -203,7 +205,10 @@ class Scanner extends BasicEmitter implements IScanner {
$data['etag_changed'] = true;
}
} else {
unset($data['unencrypted_size']);
// For new files, only preserve unencrypted_size when the file is encrypted
if (!isset($data['encrypted']) || !$data['encrypted']) {
unset($data['unencrypted_size']);
}
$newData = $data;
$fileId = -1;
}

@ -59,7 +59,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess {
private ?IUser $owner = null,
) {
$this->mount = $mount;
if (isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] !== 0) {
if (($this->data['encrypted'] ?? false) && isset($this->data['unencrypted_size'])) {
$this->rawSize = $this->data['unencrypted_size'];
} else {
$this->rawSize = $this->data['size'] ?? 0;
@ -174,7 +174,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess {
if ($includeMounts) {
$this->updateEntryFromSubMounts();
if ($this->isEncrypted() && isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) {
if ($this->isEncrypted() && isset($this->data['unencrypted_size'])) {
return $this->data['unencrypted_size'];
} else {
return isset($this->data['size']) ? 0 + $this->data['size'] : 0;
@ -356,7 +356,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess {
if (!$data) {
return;
}
$hasUnencryptedSize = isset($data['unencrypted_size']) && $data['unencrypted_size'] > 0;
$hasUnencryptedSize = !empty($data['encrypted']) && isset($data['unencrypted_size']);
if ($hasUnencryptedSize) {
$subSize = $data['unencrypted_size'];
} else {

@ -399,6 +399,7 @@ class Encryption extends Wrapper {
if ($unencryptedSize < 0
|| ($size > 0 && $unencryptedSize === $size)
|| $unencryptedSize > $size
|| ($unencryptedSize === 0 && $size > $this->util->getHeaderSize())
) {
// check if we already calculate the unencrypted size for the
// given path to avoid recursions

@ -29,7 +29,7 @@ class Encryption extends Wrapper {
protected string $cache;
protected ?int $size = null;
protected int $position;
protected ?int $unencryptedSize = null;
protected int|float|null $unencryptedSize = null;
protected int $headerSize;
protected int $unencryptedBlockSize;
protected array $header;

@ -153,6 +153,16 @@ class CacheTest extends \Test\TestCase {
$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';
@ -292,6 +302,39 @@ class CacheTest extends \Test\TestCase {
$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';

@ -29,6 +29,30 @@ class FileInfoTest extends TestCase {
$this->config = $this->getMockBuilder(IConfig::class)->getMock();
}
private function makeFileInfo(array $data): FileInfo {
$storage = new Temporary();
return new FileInfo('', $storage, '', $data, new MountPoint($storage, '/foo/files'));
}
public function testGetSizeEncryptedZeroByte(): void {
$info = $this->makeFileInfo(['encrypted' => true, 'size' => 8192, 'unencrypted_size' => 0]);
// Both paths must report the true plaintext size (0), not the on-disk encrypted size (8192)
$this->assertSame(0, $info->getSize(true));
$this->assertSame(0, $info->getSize(false));
}
public function testGetSizeEncryptedNonZero(): void {
$info = $this->makeFileInfo(['encrypted' => true, 'size' => 16384, 'unencrypted_size' => 5000]);
$this->assertSame(5000, $info->getSize(true));
$this->assertSame(5000, $info->getSize(false));
}
public function testGetSizeNonEncrypted(): void {
$info = $this->makeFileInfo(['encrypted' => false, 'size' => 100]);
$this->assertSame(100, $info->getSize(true));
$this->assertSame(100, $info->getSize(false));
}
public function testIsMountedHomeStorage(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')

@ -386,7 +386,11 @@ class EncryptionTest extends Storage {
[120, 80, false, 80],
[120, 120, false, 80],
[120, -1, false, 80],
[120, -1, true, -1]
[120, -1, true, -1],
// Zero-byte encrypted file: on-disk size equals header only (8192) — should NOT recalculate
[8192, 0, false, 0],
// Encrypted file with content beyond header but unencrypted_size=0 — SHOULD recalculate
[16384, 0, false, 80],
];
}

Loading…
Cancel
Save