Skip to content

Commit

Permalink
Loosen ContentRange validation to support zero-byte uploads (fixes #165)
Browse files Browse the repository at this point in the history
xificurk committed Oct 28, 2024
1 parent 0b9fcfc commit 73f1e1e
Showing 3 changed files with 67 additions and 6 deletions.
8 changes: 8 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -63,3 +63,11 @@ parameters:
message: "#^Calling sha1\\(\\) is forbidden, use hash\\(\\) with at least SHA\\-256 for secure hash, or password_hash\\(\\) for passwords\\.$#"
count: 1
path: src/FileUploadControl/Storage/Metadata/FileUploadMetadata.php
- # false-positive
message: "#^Method Nepada\\\\FileUploadControl\\\\Storage\\\\ContentRange\\:\\:getRangeSize\\(\\) should return int\\<0, max\\> but returns int\\.$#"
count: 1
path: src/FileUploadControl/Storage/ContentRange.php
- # should not happen
message: "#^Parameter \\#1 \\$size of static method Nepada\\\\FileUploadControl\\\\Storage\\\\ContentRange\\:\\:ofSize\\(\\) expects int\\<0, max\\>, int given\\.$#"
count: 1
path: src/FileUploadControl/Storage/FileUploadChunk.php
37 changes: 32 additions & 5 deletions src/FileUploadControl/Storage/ContentRange.php
Original file line number Diff line number Diff line change
@@ -11,10 +11,19 @@ final class ContentRange

use Nette\SmartObject;

/**
* @var int<0, max>
*/
private int $start;

/**
* @var int<0, max>
*/
private int $end;

/**
* @var int<0, max>
*/
private int $size;

private function __construct(int $start, int $end, int $size)
@@ -25,17 +34,23 @@ private function __construct(int $start, int $end, int $size)
if ($end < $start) {
throw new \InvalidArgumentException("Start ($start) cannot be larger than end ($end).");
}
if ($size <= $end) {
throw new \InvalidArgumentException("End ($end) cannot be larger or equal to size ($size).");
if ($size < $end) {
throw new \InvalidArgumentException("End ($end) cannot be larger than size ($size).");
}
if ($size > 0 && $size === $end) {
throw new \InvalidArgumentException("End ($end) cannot be equal to size ($size).");
}
$this->start = $start;
$this->end = $end;
$this->size = $size;
}

/**
* @param int<0, max> $size
*/
public static function ofSize(int $size): self
{
return new self(0, $size - 1, $size);
return new self(0, $size === 0 ? 0 : $size - 1, $size);
}

public static function fromHttpHeaderValue(string $header): self
@@ -47,21 +62,33 @@ public static function fromHttpHeaderValue(string $header): self
return new self((int) $match[1], (int) $match[2], (int) $match[3]);
}

/**
* @return int<0, max>
*/
public function getStart(): int
{
return $this->start;
}

/**
* @return int<0, max>
*/
public function getEnd(): int
{
return $this->end;
}

/**
* @return int<0, max>
*/
public function getRangeSize(): int
{
return $this->end - $this->start + 1;
return $this->size === 0 ? 0 : $this->end - $this->start + 1;
}

/**
* @return int<0, max>
*/
public function getSize(): int
{
return $this->size;
@@ -74,7 +101,7 @@ public function containsFirstByte(): bool

public function containsLastByte(): bool
{
return $this->end === ($this->size - 1);
return $this->end === max(0, ($this->size - 1));
}

}
28 changes: 27 additions & 1 deletion tests/FileUploadControl/Storage/ContentRangeTest.phpt
Original file line number Diff line number Diff line change
@@ -16,6 +16,28 @@ require_once __DIR__ . '/../../bootstrap.php';
class ContentRangeTest extends TestCase
{

public function testOfZeroSize(): void
{
$contentRange = ContentRange::ofSize(0);
Assert::same(0, $contentRange->getSize());
Assert::same(0, $contentRange->getStart());
Assert::same(0, $contentRange->getEnd());
Assert::same(0, $contentRange->getRangeSize());
Assert::true($contentRange->containsFirstByte());
Assert::true($contentRange->containsLastByte());
}

public function testOfOneByteSize(): void
{
$contentRange = ContentRange::ofSize(1);
Assert::same(1, $contentRange->getSize());
Assert::same(0, $contentRange->getStart());
Assert::same(0, $contentRange->getEnd());
Assert::same(1, $contentRange->getRangeSize());
Assert::true($contentRange->containsFirstByte());
Assert::true($contentRange->containsLastByte());
}

public function testOfSize(): void
{
$contentRange = ContentRange::ofSize(42);
@@ -62,9 +84,13 @@ class ContentRangeTest extends TestCase
'headerValue' => 'bflmpsvz',
'expectedError' => "Malformed content-range header 'bflmpsvz'.",
],
[
'headerValue' => 'bytes 0-10/5',
'expectedError' => 'End (10) cannot be larger than size (5).',
],
[
'headerValue' => 'bytes 0-10/10',
'expectedError' => 'End (10) cannot be larger or equal to size (10).',
'expectedError' => 'End (10) cannot be equal to size (10).',
],
[
'headerValue' => 'bytes 10-5/10',

0 comments on commit 73f1e1e

Please sign in to comment.