diff --git a/src/Symfony/Component/Validator/Constraints/File.php b/src/Symfony/Component/Validator/Constraints/File.php index 80527171ec..888177094c 100644 --- a/src/Symfony/Component/Validator/Constraints/File.php +++ b/src/Symfony/Component/Validator/Constraints/File.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * @Annotation @@ -24,6 +25,7 @@ use Symfony\Component\Validator\Constraint; class File extends Constraint { public $maxSize = null; + public $binaryFormat = null; public $mimeTypes = array(); public $notFoundMessage = 'The file could not be found.'; public $notReadableMessage = 'The file is not readable.'; @@ -38,4 +40,30 @@ class File extends Constraint public $uploadCantWriteErrorMessage = 'Cannot write temporary file to disk.'; public $uploadExtensionErrorMessage = 'A PHP extension caused the upload to fail.'; public $uploadErrorMessage = 'The file could not be uploaded.'; + + public function __construct($options = null) + { + parent::__construct($options); + + if ($this->maxSize) { + if (ctype_digit((string) $this->maxSize)) { + $this->maxSize = (int) $this->maxSize; + $this->binaryFormat = $this->binaryFormat === null ? false : $this->binaryFormat; + } elseif (preg_match('/^\d++k$/i', $this->maxSize)) { + $this->maxSize = $this->maxSize * 1000; + $this->binaryFormat = $this->binaryFormat === null ? false : $this->binaryFormat; + } elseif (preg_match('/^\d++M$/i', $this->maxSize)) { + $this->maxSize = $this->maxSize * 1000000; + $this->binaryFormat = $this->binaryFormat === null ? false : $this->binaryFormat; + } elseif (preg_match('/^\d++ki$/i', $this->maxSize)) { + $this->maxSize = $this->maxSize << 10; + $this->binaryFormat = $this->binaryFormat === null ? true : $this->binaryFormat; + } elseif (preg_match('/^\d++Mi$/i', $this->maxSize)) { + $this->maxSize = $this->maxSize << 20; + $this->binaryFormat = $this->binaryFormat === null ? true : $this->binaryFormat; + } else { + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size', $this->maxSize)); + } + } + } } diff --git a/src/Symfony/Component/Validator/Constraints/FileValidator.php b/src/Symfony/Component/Validator/Constraints/FileValidator.php index 22273c4dcb..99ba7e4a3d 100644 --- a/src/Symfony/Component/Validator/Constraints/FileValidator.php +++ b/src/Symfony/Component/Validator/Constraints/FileValidator.php @@ -15,7 +15,6 @@ use Symfony\Component\HttpFoundation\File\File as FileObject; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; -use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** @@ -26,13 +25,16 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; class FileValidator extends ConstraintValidator { const KB_BYTES = 1000; - const MB_BYTES = 1000000; + const KIB_BYTES = 1024; + const MIB_BYTES = 1048576; private static $suffices = array( 1 => 'bytes', self::KB_BYTES => 'kB', self::MB_BYTES => 'MB', + self::KIB_BYTES => 'KiB', + self::MIB_BYTES => 'MiB', ); /** @@ -52,16 +54,7 @@ class FileValidator extends ConstraintValidator switch ($value->getError()) { case UPLOAD_ERR_INI_SIZE: if ($constraint->maxSize) { - if (ctype_digit((string) $constraint->maxSize)) { - $limitInBytes = (int) $constraint->maxSize; - } elseif (preg_match('/^\d++k$/', $constraint->maxSize)) { - $limitInBytes = $constraint->maxSize * self::KB_BYTES; - } elseif (preg_match('/^\d++M$/', $constraint->maxSize)) { - $limitInBytes = $constraint->maxSize * self::MB_BYTES; - } else { - throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size', $constraint->maxSize)); - } - $limitInBytes = min(UploadedFile::getMaxFilesize(), $limitInBytes); + $limitInBytes = min(UploadedFile::getMaxFilesize(), (int) $constraint->maxSize); } else { $limitInBytes = UploadedFile::getMaxFilesize(); } @@ -125,24 +118,23 @@ class FileValidator extends ConstraintValidator $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)) { - $limitInBytes *= self::MB_BYTES; - } elseif (!ctype_digit((string) $constraint->maxSize)) { - throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size', $constraint->maxSize)); - } - if ($sizeInBytes > $limitInBytes) { // Convert the limit to the smallest possible number // (i.e. try "MB", then "kB", then "bytes") - $coef = self::MB_BYTES; + if ($constraint->binaryFormat) { + $coef = self::MIB_BYTES; + $coefFactor = self::KIB_BYTES; + } else { + $coef = self::MB_BYTES; + $coefFactor = self::KB_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; + $coef /= $coefFactor; $limitAsString = (string) ($limitInBytes / $coef); } @@ -152,7 +144,7 @@ class FileValidator extends ConstraintValidator // If the size and limit produce the same string output // (due to rounding), reduce the coefficient while ($sizeAsString === $limitAsString) { - $coef /= 1000; + $coef /= $coefFactor; $limitAsString = (string) ($limitInBytes / $coef); $sizeAsString = (string) round($sizeInBytes / $coef, 2); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/FileTest.php b/src/Symfony/Component/Validator/Tests/Constraints/FileTest.php new file mode 100644 index 0000000000..801de83993 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/FileTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\File; + +class FileTest extends \PHPUnit_Framework_TestCase +{ + + /** + * @param mixed $maxSize + * @param int bytes + * @param bool $bytes + * @dataProvider provideValidSizes + */ + public function testMaxSize($maxSize, $bytes, $binaryFormat) + { + $file = new File(array('maxSize' => $maxSize)); + + $this->assertSame($bytes, $file->maxSize); + $this->assertSame($binaryFormat, $file->binaryFormat); + } + + /** + * @param mixed $maxSize + * @param int $bytes + * @dataProvider provideInValidSizes + * @expectedException Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testInvalideMaxSize($maxSize) + { + $file = new File(array('maxSize' => $maxSize)); + } + + /** + * @return array + */ + public function provideValidSizes() + { + return array( + array('500', 500, false), + array(12300, 12300, false), + array('1ki', 1024, true), + array('1KI', 1024, true), + array('2k', 2000, false), + array('2K', 2000, false), + array('1mi', 1048576, true), + array('1MI', 1048576, true), + array('3m', 3000000, false), + array('3M', 3000000, false), + ); + } + + /** + * @return array + */ + public function provideInvalidSizes() + { + return array( + array('+100'), + array('foo'), + array('1Ko'), + array('1kio'), + array('1G'), + array('1Gi'), + ); + } + + /** + * @param mixed $maxSize + * @param bool $guessedFormat + * @param bool $binaryFormat + * @dataProvider provideFormats + */ + public function testBinaryFormat($maxSize, $guessedFormat, $binaryFormat) + { + $file = new File(array('maxSize' => $maxSize, 'binaryFormat' => $guessedFormat)); + + $this->assertSame($binaryFormat, $file->binaryFormat); + } + + /** + * @return array + */ + public function provideFormats() + { + return array( + array(100, null, false), + array(100, true, true), + array(100, false, false), + array('100K', null, false), + array('100K', true, true), + array('100K', false, false), + array('100Ki', null, true), + array('100Ki', true, true), + array('100Ki', false, false), + ); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php index d2d2bcb34f..3b12a4c257 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php @@ -99,27 +99,36 @@ abstract class FileValidatorTest extends \PHPUnit_Framework_TestCase // 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'), + array(ceil(1.005*1024), '1Ki', '1.01', '1', 'KiB'), // 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(ceil(1.004*1024), '1Ki', '1029', '1024', 'bytes'), array(1000 + 1, 1000, '1001', '1000', 'bytes'), array(1000 + 1, '1k', '1001', '1000', 'bytes'), + array(1024 + 1, '1Ki', '1025', '1024', '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'), + array(ceil(1.005*1024*1024), '1024Ki', '1.01', '1', 'MiB'), + array(ceil(1.005*1024*1024), '1Mi', '1.01', '1', 'MiB'), // 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(ceil(1.004*1024*1024), '1024Ki', '1028.1', '1024', 'KiB'), + array(ceil(1.004*1024*1024), '1Mi', '1028.1', '1024', 'KiB'), 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'), + array(1024*1024 + 1, '1024Ki', '1048577', '1048576', 'bytes'), + array(1024*1024 + 1, '1Mi', '1048577', '1048576', 'bytes'), ); } @@ -157,9 +166,13 @@ abstract class FileValidatorTest extends \PHPUnit_Framework_TestCase array(1000, '1k'), array(1000 - 1, '1k'), + array(1024, '1Ki'), + array(1024 - 1, '1Ki'), array(1000*1000, '1M'), array(1000*1000 - 1, '1M'), + array(1024*1024, '1Mi'), + array(1024*1024 - 1, '1Mi'), ); } @@ -195,6 +208,55 @@ abstract class FileValidatorTest extends \PHPUnit_Framework_TestCase $this->validator->validate($this->path, $constraint); } + public function provideBinaryFormatTests() + { + return array( + array(11, 10, null, '11', '10', 'bytes'), + array(11, 10, true, '11', '10', 'bytes'), + array(11, 10, false, '11', '10', 'bytes'), + + // round(size) == 1.01kB, limit == 1kB + array(ceil(1000*1.01), 1000, null, '1.01', '1', 'kB'), + array(ceil(1000*1.01), '1k', null, '1.01', '1', 'kB'), + array(ceil(1024*1.01), '1Ki', null, '1.01', '1', 'KiB'), + + array(ceil(1024*1.01), 1024, true, '1.01', '1', 'KiB'), + array(ceil(1024*1.01*1000), '1024k', true, '1010', '1000', 'KiB'), + array(ceil(1024*1.01), '1Ki', true, '1.01', '1', 'KiB'), + + array(ceil(1000*1.01), 1000, false, '1.01', '1', 'kB'), + array(ceil(1000*1.01), '1k', false, '1.01', '1', 'kB'), + array(ceil(1024*1.01*10), '10Ki', false, '10.34', '10.24', 'kB'), + ); + } + + /** + * @dataProvider provideBinaryFormatTests + */ + public function testBinaryFormat($bytesWritten, $limit, $binaryFormat, $sizeAsString, $limitAsString, $suffix) + { + fseek($this->file, $bytesWritten-1, SEEK_SET); + fwrite($this->file, '0'); + fclose($this->file); + + $constraint = new File(array( + 'maxSize' => $limit, + 'binaryFormat' => $binaryFormat, + 'maxSizeMessage' => 'myMessage', + )); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('myMessage', array( + '{{ limit }}' => $limitAsString, + '{{ size }}' => $sizeAsString, + '{{ suffix }}' => $suffix, + '{{ file }}' => $this->path, + )); + + $this->validator->validate($this->getFile($this->path), $constraint); + } + public function testValidMimeType() { $file = $this