Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 38 additions & 19 deletions lib/private/Files/Stream/Quota.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
36 changes: 36 additions & 0 deletions tests/lib/Files/Stream/QuotaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
Loading