Skip to content

Commit 22e6b68

Browse files
Jérémy Derusséfabpot
authored andcommitted
[Validator] Support "maxSize" given in KiB
1 parent 4590252 commit 22e6b68

File tree

4 files changed

+213
-23
lines changed

4 files changed

+213
-23
lines changed

Constraints/File.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Validator\Constraints;
1313

1414
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
1516

1617
/**
1718
* @Annotation
@@ -24,6 +25,7 @@
2425
class File extends Constraint
2526
{
2627
public $maxSize = null;
28+
public $binaryFormat = null;
2729
public $mimeTypes = array();
2830
public $notFoundMessage = 'The file could not be found.';
2931
public $notReadableMessage = 'The file is not readable.';
@@ -38,4 +40,30 @@ class File extends Constraint
3840
public $uploadCantWriteErrorMessage = 'Cannot write temporary file to disk.';
3941
public $uploadExtensionErrorMessage = 'A PHP extension caused the upload to fail.';
4042
public $uploadErrorMessage = 'The file could not be uploaded.';
43+
44+
public function __construct($options = null)
45+
{
46+
parent::__construct($options);
47+
48+
if ($this->maxSize) {
49+
if (ctype_digit((string) $this->maxSize)) {
50+
$this->maxSize = (int) $this->maxSize;
51+
$this->binaryFormat = $this->binaryFormat === null ? false : $this->binaryFormat;
52+
} elseif (preg_match('/^\d++k$/i', $this->maxSize)) {
53+
$this->maxSize = $this->maxSize * 1000;
54+
$this->binaryFormat = $this->binaryFormat === null ? false : $this->binaryFormat;
55+
} elseif (preg_match('/^\d++M$/i', $this->maxSize)) {
56+
$this->maxSize = $this->maxSize * 1000000;
57+
$this->binaryFormat = $this->binaryFormat === null ? false : $this->binaryFormat;
58+
} elseif (preg_match('/^\d++ki$/i', $this->maxSize)) {
59+
$this->maxSize = $this->maxSize << 10;
60+
$this->binaryFormat = $this->binaryFormat === null ? true : $this->binaryFormat;
61+
} elseif (preg_match('/^\d++Mi$/i', $this->maxSize)) {
62+
$this->maxSize = $this->maxSize << 20;
63+
$this->binaryFormat = $this->binaryFormat === null ? true : $this->binaryFormat;
64+
} else {
65+
throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size', $this->maxSize));
66+
}
67+
}
68+
}
4169
}

Constraints/FileValidator.php

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use Symfony\Component\HttpFoundation\File\UploadedFile;
1616
use Symfony\Component\Validator\Constraint;
1717
use Symfony\Component\Validator\ConstraintValidator;
18-
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
1918
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
2019

2120
/**
@@ -26,13 +25,16 @@
2625
class FileValidator extends ConstraintValidator
2726
{
2827
const KB_BYTES = 1000;
29-
3028
const MB_BYTES = 1000000;
29+
const KIB_BYTES = 1024;
30+
const MIB_BYTES = 1048576;
3131

3232
private static $suffices = array(
3333
1 => 'bytes',
3434
self::KB_BYTES => 'kB',
3535
self::MB_BYTES => 'MB',
36+
self::KIB_BYTES => 'KiB',
37+
self::MIB_BYTES => 'MiB',
3638
);
3739

3840
/**
@@ -52,16 +54,7 @@ public function validate($value, Constraint $constraint)
5254
switch ($value->getError()) {
5355
case UPLOAD_ERR_INI_SIZE:
5456
if ($constraint->maxSize) {
55-
if (ctype_digit((string) $constraint->maxSize)) {
56-
$limitInBytes = (int) $constraint->maxSize;
57-
} elseif (preg_match('/^\d++k$/', $constraint->maxSize)) {
58-
$limitInBytes = $constraint->maxSize * self::KB_BYTES;
59-
} elseif (preg_match('/^\d++M$/', $constraint->maxSize)) {
60-
$limitInBytes = $constraint->maxSize * self::MB_BYTES;
61-
} else {
62-
throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size', $constraint->maxSize));
63-
}
64-
$limitInBytes = min(UploadedFile::getMaxFilesize(), $limitInBytes);
57+
$limitInBytes = min(UploadedFile::getMaxFilesize(), (int) $constraint->maxSize);
6558
} else {
6659
$limitInBytes = UploadedFile::getMaxFilesize();
6760
}
@@ -125,24 +118,23 @@ public function validate($value, Constraint $constraint)
125118
$sizeInBytes = filesize($path);
126119
$limitInBytes = (int) $constraint->maxSize;
127120

128-
if (preg_match('/^\d++k$/', $constraint->maxSize)) {
129-
$limitInBytes *= self::KB_BYTES;
130-
} elseif (preg_match('/^\d++M$/', $constraint->maxSize)) {
131-
$limitInBytes *= self::MB_BYTES;
132-
} elseif (!ctype_digit((string) $constraint->maxSize)) {
133-
throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size', $constraint->maxSize));
134-
}
135-
136121
if ($sizeInBytes > $limitInBytes) {
137122
// Convert the limit to the smallest possible number
138123
// (i.e. try "MB", then "kB", then "bytes")
139-
$coef = self::MB_BYTES;
124+
if ($constraint->binaryFormat) {
125+
$coef = self::MIB_BYTES;
126+
$coefFactor = self::KIB_BYTES;
127+
} else {
128+
$coef = self::MB_BYTES;
129+
$coefFactor = self::KB_BYTES;
130+
}
131+
140132
$limitAsString = (string) ($limitInBytes / $coef);
141133

142134
// Restrict the limit to 2 decimals (without rounding! we
143135
// need the precise value)
144136
while (self::moreDecimalsThan($limitAsString, 2)) {
145-
$coef /= 1000;
137+
$coef /= $coefFactor;
146138
$limitAsString = (string) ($limitInBytes / $coef);
147139
}
148140

@@ -152,7 +144,7 @@ public function validate($value, Constraint $constraint)
152144
// If the size and limit produce the same string output
153145
// (due to rounding), reduce the coefficient
154146
while ($sizeAsString === $limitAsString) {
155-
$coef /= 1000;
147+
$coef /= $coefFactor;
156148
$limitAsString = (string) ($limitInBytes / $coef);
157149
$sizeAsString = (string) round($sizeInBytes / $coef, 2);
158150
}

Tests/Constraints/FileTest.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Constraints;
13+
14+
use Symfony\Component\Validator\Constraints\File;
15+
16+
class FileTest extends \PHPUnit_Framework_TestCase
17+
{
18+
19+
/**
20+
* @param mixed $maxSize
21+
* @param int bytes
22+
* @param bool $bytes
23+
* @dataProvider provideValidSizes
24+
*/
25+
public function testMaxSize($maxSize, $bytes, $binaryFormat)
26+
{
27+
$file = new File(array('maxSize' => $maxSize));
28+
29+
$this->assertSame($bytes, $file->maxSize);
30+
$this->assertSame($binaryFormat, $file->binaryFormat);
31+
}
32+
33+
/**
34+
* @param mixed $maxSize
35+
* @param int $bytes
36+
* @dataProvider provideInValidSizes
37+
* @expectedException Symfony\Component\Validator\Exception\ConstraintDefinitionException
38+
*/
39+
public function testInvalideMaxSize($maxSize)
40+
{
41+
$file = new File(array('maxSize' => $maxSize));
42+
}
43+
44+
/**
45+
* @return array
46+
*/
47+
public function provideValidSizes()
48+
{
49+
return array(
50+
array('500', 500, false),
51+
array(12300, 12300, false),
52+
array('1ki', 1024, true),
53+
array('1KI', 1024, true),
54+
array('2k', 2000, false),
55+
array('2K', 2000, false),
56+
array('1mi', 1048576, true),
57+
array('1MI', 1048576, true),
58+
array('3m', 3000000, false),
59+
array('3M', 3000000, false),
60+
);
61+
}
62+
63+
/**
64+
* @return array
65+
*/
66+
public function provideInvalidSizes()
67+
{
68+
return array(
69+
array('+100'),
70+
array('foo'),
71+
array('1Ko'),
72+
array('1kio'),
73+
array('1G'),
74+
array('1Gi'),
75+
);
76+
}
77+
78+
/**
79+
* @param mixed $maxSize
80+
* @param bool $guessedFormat
81+
* @param bool $binaryFormat
82+
* @dataProvider provideFormats
83+
*/
84+
public function testBinaryFormat($maxSize, $guessedFormat, $binaryFormat)
85+
{
86+
$file = new File(array('maxSize' => $maxSize, 'binaryFormat' => $guessedFormat));
87+
88+
$this->assertSame($binaryFormat, $file->binaryFormat);
89+
}
90+
91+
/**
92+
* @return array
93+
*/
94+
public function provideFormats()
95+
{
96+
return array(
97+
array(100, null, false),
98+
array(100, true, true),
99+
array(100, false, false),
100+
array('100K', null, false),
101+
array('100K', true, true),
102+
array('100K', false, false),
103+
array('100Ki', null, true),
104+
array('100Ki', true, true),
105+
array('100Ki', false, false),
106+
);
107+
}
108+
}

Tests/Constraints/FileValidatorTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,27 +99,36 @@ public function provideMaxSizeExceededTests()
9999
// round(size) == 1.01kB, limit == 1kB
100100
array(ceil(1.005*1000), 1000, '1.01', '1', 'kB'),
101101
array(ceil(1.005*1000), '1k', '1.01', '1', 'kB'),
102+
array(ceil(1.005*1024), '1Ki', '1.01', '1', 'KiB'),
102103

103104
// round(size) == 1kB, limit == 1kB -> use bytes
104105
array(ceil(1.004*1000), 1000, '1004', '1000', 'bytes'),
105106
array(ceil(1.004*1000), '1k', '1004', '1000', 'bytes'),
107+
array(ceil(1.004*1024), '1Ki', '1029', '1024', 'bytes'),
106108

107109
array(1000 + 1, 1000, '1001', '1000', 'bytes'),
108110
array(1000 + 1, '1k', '1001', '1000', 'bytes'),
111+
array(1024 + 1, '1Ki', '1025', '1024', 'bytes'),
109112

110113
// round(size) == 1.01MB, limit == 1MB
111114
array(ceil(1.005*1000*1000), 1000*1000, '1.01', '1', 'MB'),
112115
array(ceil(1.005*1000*1000), '1000k', '1.01', '1', 'MB'),
113116
array(ceil(1.005*1000*1000), '1M', '1.01', '1', 'MB'),
117+
array(ceil(1.005*1024*1024), '1024Ki', '1.01', '1', 'MiB'),
118+
array(ceil(1.005*1024*1024), '1Mi', '1.01', '1', 'MiB'),
114119

115120
// round(size) == 1MB, limit == 1MB -> use kB
116121
array(ceil(1.004*1000*1000), 1000*1000, '1004', '1000', 'kB'),
117122
array(ceil(1.004*1000*1000), '1000k', '1004', '1000', 'kB'),
118123
array(ceil(1.004*1000*1000), '1M', '1004', '1000', 'kB'),
124+
array(ceil(1.004*1024*1024), '1024Ki', '1028.1', '1024', 'KiB'),
125+
array(ceil(1.004*1024*1024), '1Mi', '1028.1', '1024', 'KiB'),
119126

120127
array(1000*1000 + 1, 1000*1000, '1000001', '1000000', 'bytes'),
121128
array(1000*1000 + 1, '1000k', '1000001', '1000000', 'bytes'),
122129
array(1000*1000 + 1, '1M', '1000001', '1000000', 'bytes'),
130+
array(1024*1024 + 1, '1024Ki', '1048577', '1048576', 'bytes'),
131+
array(1024*1024 + 1, '1Mi', '1048577', '1048576', 'bytes'),
123132
);
124133
}
125134

@@ -157,9 +166,13 @@ public function provideMaxSizeNotExceededTests()
157166

158167
array(1000, '1k'),
159168
array(1000 - 1, '1k'),
169+
array(1024, '1Ki'),
170+
array(1024 - 1, '1Ki'),
160171

161172
array(1000*1000, '1M'),
162173
array(1000*1000 - 1, '1M'),
174+
array(1024*1024, '1Mi'),
175+
array(1024*1024 - 1, '1Mi'),
163176
);
164177
}
165178

@@ -195,6 +208,55 @@ public function testInvalidMaxSize()
195208
$this->validator->validate($this->path, $constraint);
196209
}
197210

211+
public function provideBinaryFormatTests()
212+
{
213+
return array(
214+
array(11, 10, null, '11', '10', 'bytes'),
215+
array(11, 10, true, '11', '10', 'bytes'),
216+
array(11, 10, false, '11', '10', 'bytes'),
217+
218+
// round(size) == 1.01kB, limit == 1kB
219+
array(ceil(1000*1.01), 1000, null, '1.01', '1', 'kB'),
220+
array(ceil(1000*1.01), '1k', null, '1.01', '1', 'kB'),
221+
array(ceil(1024*1.01), '1Ki', null, '1.01', '1', 'KiB'),
222+
223+
array(ceil(1024*1.01), 1024, true, '1.01', '1', 'KiB'),
224+
array(ceil(1024*1.01*1000), '1024k', true, '1010', '1000', 'KiB'),
225+
array(ceil(1024*1.01), '1Ki', true, '1.01', '1', 'KiB'),
226+
227+
array(ceil(1000*1.01), 1000, false, '1.01', '1', 'kB'),
228+
array(ceil(1000*1.01), '1k', false, '1.01', '1', 'kB'),
229+
array(ceil(1024*1.01*10), '10Ki', false, '10.34', '10.24', 'kB'),
230+
);
231+
}
232+
233+
/**
234+
* @dataProvider provideBinaryFormatTests
235+
*/
236+
public function testBinaryFormat($bytesWritten, $limit, $binaryFormat, $sizeAsString, $limitAsString, $suffix)
237+
{
238+
fseek($this->file, $bytesWritten-1, SEEK_SET);
239+
fwrite($this->file, '0');
240+
fclose($this->file);
241+
242+
$constraint = new File(array(
243+
'maxSize' => $limit,
244+
'binaryFormat' => $binaryFormat,
245+
'maxSizeMessage' => 'myMessage',
246+
));
247+
248+
$this->context->expects($this->once())
249+
->method('addViolation')
250+
->with('myMessage', array(
251+
'{{ limit }}' => $limitAsString,
252+
'{{ size }}' => $sizeAsString,
253+
'{{ suffix }}' => $suffix,
254+
'{{ file }}' => $this->path,
255+
));
256+
257+
$this->validator->validate($this->getFile($this->path), $constraint);
258+
}
259+
198260
public function testValidMimeType()
199261
{
200262
$file = $this

0 commit comments

Comments
 (0)