[Validator] Improved to-string conversion of the file size/size limit

This commit is contained in:
Bernhard Schussek 2014-05-20 16:38:02 +02:00
parent bbe1045989
commit e4c6da548b
2 changed files with 132 additions and 85 deletions

View File

@ -25,6 +25,16 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
*/
class FileValidator extends ConstraintValidator
{
const KB_BYTES = 1000;
const MB_BYTES = 1000000;
private static $suffices = array(
1 => 'bytes',
self::KB_BYTES => 'kB',
self::MB_BYTES => 'MB',
);
/**
* {@inheritdoc}
*/
@ -43,21 +53,21 @@ class FileValidator extends ConstraintValidator
case UPLOAD_ERR_INI_SIZE:
if ($constraint->maxSize) {
if (ctype_digit((string) $constraint->maxSize)) {
$maxSize = (int) $constraint->maxSize;
$limitInBytes = (int) $constraint->maxSize;
} elseif (preg_match('/^\d++k$/', $constraint->maxSize)) {
$maxSize = $constraint->maxSize * 1000;
$limitInBytes = $constraint->maxSize * self::KB_BYTES;
} elseif (preg_match('/^\d++M$/', $constraint->maxSize)) {
$maxSize = $constraint->maxSize * 1000 * 1000;
$limitInBytes = $constraint->maxSize * self::MB_BYTES;
} else {
throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size', $constraint->maxSize));
}
$maxSize = min(UploadedFile::getMaxFilesize(), $maxSize);
$limitInBytes = min(UploadedFile::getMaxFilesize(), $limitInBytes);
} else {
$maxSize = UploadedFile::getMaxFilesize();
$limitInBytes = UploadedFile::getMaxFilesize();
}
$this->context->addViolation($constraint->uploadIniSizeErrorMessage, array(
'{{ limit }}' => $maxSize,
'{{ limit }}' => $limitInBytes,
'{{ suffix }}' => 'bytes',
));
@ -112,27 +122,45 @@ class FileValidator extends ConstraintValidator
}
if ($constraint->maxSize) {
if (ctype_digit((string) $constraint->maxSize)) {
$size = filesize($path);
$limit = (int) $constraint->maxSize;
$suffix = 'bytes';
} elseif (preg_match('/^\d++k$/', $constraint->maxSize)) {
$size = round(filesize($path) / 1000, 2);
$limit = (int) $constraint->maxSize;
$suffix = 'KB';
$sizeInBytes = filesize($path);
$limitInBytes = (int) $constraint->maxSize;
if (preg_match('/^\d++k$/', $constraint->maxSize)) {
$limitInBytes *= self::KB_BYTES;
} elseif (preg_match('/^\d++M$/', $constraint->maxSize)) {
$size = round(filesize($path) / (1000 * 1000), 2);
$limit = (int) $constraint->maxSize;
$suffix = 'MB';
} else {
$limitInBytes *= self::MB_BYTES;
} elseif (!ctype_digit((string) $constraint->maxSize)) {
throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size', $constraint->maxSize));
}
if ($size > $limit) {
if ($sizeInBytes > $limitInBytes) {
// Convert the limit to the smallest possible number
// (i.e. try "MB", then "kB", then "bytes")
$coef = self::MB_BYTES;
$limitAsString = (string) ($limitInBytes / $coef);
// Restrict the limit to 2 decimals (without rounding! we
// need the precise value)
while (self::moreDecimalsThan($limitAsString, 2)) {
$coef /= 1000;
$limitAsString = (string) ($limitInBytes / $coef);
}
// Convert size to the same measure, but round to 2 decimals
$sizeAsString = (string) round($sizeInBytes / $coef, 2);
// If the size and limit produce the same string output
// (due to rounding), reduce the coefficient
while ($sizeAsString === $limitAsString) {
$coef /= 1000;
$limitAsString = (string) ($limitInBytes / $coef);
$sizeAsString = (string) round($sizeInBytes / $coef, 2);
}
$this->context->addViolation($constraint->maxSizeMessage, array(
'{{ size }}' => $size,
'{{ limit }}' => $limit,
'{{ suffix }}' => $suffix,
'{{ size }}' => $sizeAsString,
'{{ limit }}' => $limitAsString,
'{{ suffix }}' => static::$suffices[$coef],
'{{ file }}' => $path,
));
@ -172,4 +200,9 @@ class FileValidator extends ConstraintValidator
}
}
}
private static function moreDecimalsThan($double, $numberOfDecimals)
{
return strlen((string) $double) > strlen(round($double, $numberOfDecimals));
}
}

View File

@ -33,7 +33,13 @@ abstract class FileValidatorTest extends \PHPUnit_Framework_TestCase
protected function tearDown()
{
fclose($this->file);
if (is_resource($this->file)) {
fclose($this->file);
}
if (file_exists($this->path)) {
unlink($this->path);
}
$this->context = null;
$this->validator = null;
@ -82,93 +88,101 @@ abstract class FileValidatorTest extends \PHPUnit_Framework_TestCase
$this->validator->validate($file, new File());
}
public function testTooLargeBytes()
public function provideMaxSizeExceededTests()
{
fwrite($this->file, str_repeat('0', 11));
return array(
array(11, 10, '11', '10', 'bytes'),
array(ceil(1.005*1000), ceil(1.005*1000) - 1, '1005', '1004', 'bytes'),
array(ceil(1.005*1000*1000), ceil(1.005*1000*1000) - 1, '1005000', '1004999', 'bytes'),
// round(size) == 1.01kB, limit == 1kB
array(ceil(1.005*1000), 1000, '1.01', '1', 'kB'),
array(ceil(1.005*1000), '1k', '1.01', '1', 'kB'),
// round(size) == 1kB, limit == 1kB -> use bytes
array(ceil(1.004*1000), 1000, '1004', '1000', 'bytes'),
array(ceil(1.004*1000), '1k', '1004', '1000', 'bytes'),
array(1000 + 1, 1000, '1001', '1000', 'bytes'),
array(1000 + 1, '1k', '1001', '1000', 'bytes'),
// round(size) == 1.01MB, limit == 1MB
array(ceil(1.005*1000*1000), 1000*1000, '1.01', '1', 'MB'),
array(ceil(1.005*1000*1000), '1000k', '1.01', '1', 'MB'),
array(ceil(1.005*1000*1000), '1M', '1.01', '1', 'MB'),
// round(size) == 1MB, limit == 1MB -> use kB
array(ceil(1.004*1000*1000), 1000*1000, '1004', '1000', 'kB'),
array(ceil(1.004*1000*1000), '1000k', '1004', '1000', 'kB'),
array(ceil(1.004*1000*1000), '1M', '1004', '1000', 'kB'),
array(1000*1000 + 1, 1000*1000, '1000001', '1000000', 'bytes'),
array(1000*1000 + 1, '1000k', '1000001', '1000000', 'bytes'),
array(1000*1000 + 1, '1M', '1000001', '1000000', 'bytes'),
);
}
/**
* @dataProvider provideMaxSizeExceededTests
*/
public function testMaxSizeExceeded($bytesWritten, $limit, $sizeAsString, $limitAsString, $suffix)
{
fseek($this->file, $bytesWritten-1, SEEK_SET);
fwrite($this->file, '0');
fclose($this->file);
$constraint = new File(array(
'maxSize' => 10,
'maxSize' => $limit,
'maxSizeMessage' => 'myMessage',
));
$this->context->expects($this->once())
->method('addViolation')
->with('myMessage', array(
'{{ limit }}' => '10',
'{{ size }}' => '11',
'{{ suffix }}' => 'bytes',
'{{ limit }}' => $limitAsString,
'{{ size }}' => $sizeAsString,
'{{ suffix }}' => $suffix,
'{{ file }}' => $this->path,
));
$this->validator->validate($this->getFile($this->path), $constraint);
}
public function testTooLargeKiloBytes()
public function provideMaxSizeNotExceededTests()
{
fwrite($this->file, str_repeat('0', 1400));
return array(
array(10, 10),
array(9, 10),
array(1000, '1k'),
array(1000 - 1, '1k'),
array(1000*1000, '1M'),
array(1000*1000 - 1, '1M'),
);
}
/**
* @dataProvider provideMaxSizeNotExceededTests
*/
public function testMaxSizeNotExceeded($bytesWritten, $limit)
{
fseek($this->file, $bytesWritten-1, SEEK_SET);
fwrite($this->file, '0');
fclose($this->file);
$constraint = new File(array(
'maxSize' => '1k',
'maxSize' => $limit,
'maxSizeMessage' => 'myMessage',
));
$this->context->expects($this->once())
->method('addViolation')
->with('myMessage', array(
'{{ limit }}' => '1',
'{{ size }}' => '1.37',
'{{ suffix }}' => 'KiB',
'{{ file }}' => $this->path,
));
$this->context->expects($this->never())
->method('addViolation');
$this->validator->validate($this->getFile($this->path), $constraint);
}
public function testTooLargeMegaBytes()
{
fwrite($this->file, str_repeat('0', 1400000));
$constraint = new File(array(
'maxSize' => '1M',
'maxSizeMessage' => 'myMessage',
));
$this->context->expects($this->once())
->method('addViolation')
->with('myMessage', array(
'{{ limit }}' => '1',
'{{ size }}' => '1.34',
'{{ suffix }}' => 'MiB',
'{{ file }}' => $this->path,
));
$this->validator->validate($this->getFile($this->path), $constraint);
}
public function testMaxSizeKiloBytes()
{
fwrite($this->file, str_repeat('0', 1010));
$constraint = new File(array(
'maxSize' => '1k',
));
$this->context->expects($this->never())->method('addViolation');
$this->validator->validate($this->getFile($this->path), $constraint);
}
public function testMaxSizeMegaBytes()
{
fwrite($this->file, str_repeat('0', (1024 * 1022)));
$constraint = new File(array(
'maxSize' => '1M',
));
$this->context->expects($this->never())->method('addViolation');
$this->validator->validate($this->getFile($this->path), $constraint);
}
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/