diff --git a/lib/private/Files/Stream/Quota.php b/lib/private/Files/Stream/Quota.php index 3c7e09c633bc7..79d651d400edb 100644 --- a/lib/private/Files/Stream/Quota.php +++ b/lib/private/Files/Stream/Quota.php @@ -48,40 +48,59 @@ public function dir_opendir($path, $options) { } public function stream_seek($offset, $whence = SEEK_SET) { - if ($whence === SEEK_END) { - // go to the end to find out last position's offset - $oldOffset = $this->stream_tell(); - if (fseek($this->source, 0, $whence) !== 0) { - return false; - } - $whence = SEEK_SET; - $offset = $this->stream_tell() + $offset; - $this->limit += $oldOffset - $offset; - } elseif ($whence === SEEK_SET) { - $this->limit += $this->stream_tell() - $offset; - } else { - $this->limit -= $offset; - } // this wrapper needs to return "true" for success. // the fseek call itself returns 0 on succeess return fseek($this->source, $offset, $whence) === 0; } public function stream_read($count) { - $this->limit -= $count; + // Do not decrement $this->limit for reads return fread($this->source, $count); } public function stream_write($data) { $size = strlen($data); - if ($size > $this->limit) { - $data = substr($data, 0, $this->limit); - $size = $this->limit; + + // Get current pointer and file size + $curPos = ftell($this->source); + fseek($this->source, 0, SEEK_END); + $fileSize = ftell($this->source); + fseek($this->source, $curPos, SEEK_SET); + + $writeEnd = $curPos + $size; + + // Calculate how many bytes are "new" (beyond end of existing) + $newBytes = max(0, $writeEnd - $fileSize); + + // Enforce quota for new bytes only + if ($newBytes > $this->limit) { + // Only this many new bytes are permitted: + $allowedNewBytes = $this->limit; + // Adjust write size to fit quota + // Calculate max amount we can write, given cursor position and allowed new bytes + $allowedSize = $size - ($newBytes - $allowedNewBytes); + + if ($allowedSize <= 0) { + return 0; // No new bytes allowed + } + $data = substr($data, 0, $allowedSize); + $size = $allowedSize; + // Recalculate position/write end after truncation (for safety) + $writeEnd = $curPos + $size; + $newBytes = max(0, $writeEnd - $fileSize); } + $written = fwrite($this->source, $data); + + // Decrement limit by actually written new bytes + // (Extra safety: recalculate actual new bytes in case fwrite was truncated) + $actualWriteEnd = ftell($this->source); + $actualFileSize = max($fileSize, $actualWriteEnd); + $actualNewBytes = max(0, $actualFileSize - $fileSize); // Decrement quota by the actual number of bytes written ($written), // not the intended size - $this->limit -= $written; + $this->limit -= $actualNewBytes; + return $written; } } diff --git a/tests/lib/Files/Stream/QuotaTest.php b/tests/lib/Files/Stream/QuotaTest.php index 4248d14f5a1fe..c1ddcd1546d13 100644 --- a/tests/lib/Files/Stream/QuotaTest.php +++ b/tests/lib/Files/Stream/QuotaTest.php @@ -28,6 +28,42 @@ public function testWriteEnoughSpace(): void { $this->assertEquals('foobar', fread($stream, 100)); } + // Quota is not reduced by reads or seeks + public function testReadsAndSeeksDoNotAffectQuota() { + $stream = $this->getStream('w+', 6); + $this->assertEquals('', fread($stream, 3)); // Read before write + $this->assertEquals(6, fwrite($stream, 'foobar')); // Write to fill quota + rewind($stream); + $this->assertEquals('foo', fread($stream, 3)); + fseek($stream, 1, SEEK_CUR); // Seek should not affect quota + $this->assertEquals(0, fwrite($stream, 'baz')); // No quota left for growth + rewind($stream); + $this->assertEquals('foobar', fread($stream, 100)); + } + + // Overwriting after quota is exhausted should succeed + public function testOverwriteAfterQuotaExhausted() { + $stream = $this->getStream('w+', 3); + fwrite($stream, 'abc'); // Fill quota + $this->assertEquals(0, fwrite($stream, 'd')); // No quota for growth + rewind($stream); + $this->assertEquals(3, fwrite($stream, 'xyz')); // Overwrite: should succeed + rewind($stream); + $this->assertEquals('xyz', fread($stream, 100)); + } + + // Quota is only used for file growth + public function testQuotaOnlyUsedForGrowth() { + $stream = $this->getStream('w+', 5); + $this->assertEquals(5, fwrite($stream, '12345')); // Fill quota + rewind($stream); + $this->assertEquals(3, fwrite($stream, 'abc')); // Overwrite allowed + fseek($stream, 5, SEEK_SET); + $this->assertEquals(0, fwrite($stream, 'xx')); // No quota for extension + rewind($stream); + $this->assertEquals('abc45', fread($stream, 100)); + } + public function testWriteNotEnoughSpace(): void { $stream = $this->getStream('w+', 3); $this->assertEquals(3, fwrite($stream, 'foobar'));