Skip to content

Commit 914b354

Browse files
committed
feat: binary file size validation support
1 parent bac0903 commit 914b354

File tree

2 files changed

+826
-41
lines changed

2 files changed

+826
-41
lines changed

src/Illuminate/Validation/Rules/File.php

Lines changed: 151 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule
1717
{
1818
use Conditionable, Macroable;
1919

20+
/**
21+
* Binary units flag used for size validation.
22+
*/
23+
public const BINARY = 'binary';
24+
25+
/**
26+
* International units flag used for size validation.
27+
*/
28+
public const INTERNATIONAL = 'international';
29+
2030
/**
2131
* The MIME types that the given file should match. This array may also contain file extensions.
2232
*
@@ -45,6 +55,11 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule
4555
*/
4656
protected $maximumFileSize = null;
4757

58+
/**
59+
* The units used for size validation.
60+
*/
61+
protected string $units = self::INTERNATIONAL;
62+
4863
/**
4964
* An array of custom rules that will be merged into the validation rules.
5065
*
@@ -150,84 +165,175 @@ public function extensions($extensions)
150165
return $this;
151166
}
152167

168+
/**
169+
* Set the units for size validation to binary.
170+
*/
171+
public function binary(): static
172+
{
173+
$this->units = self::BINARY;
174+
return $this;
175+
}
176+
177+
/**
178+
* Set the units for size validation to international.
179+
*/
180+
public function international(): static
181+
{
182+
$this->units = self::INTERNATIONAL;
183+
return $this;
184+
}
185+
186+
187+
153188
/**
154189
* Indicate that the uploaded file should be exactly a certain size in kilobytes.
155-
*
156-
* @param string|int $size
157-
* @return $this
158190
*/
159-
public function size($size)
191+
public function size(string|int $size, ?string $units = null): static
160192
{
161-
$this->minimumFileSize = $this->toKilobytes($size);
193+
$this->minimumFileSize = $this->toKilobytes($size, $this->units($units));
162194
$this->maximumFileSize = $this->minimumFileSize;
163195

164196
return $this;
165197
}
166198

167199
/**
168200
* Indicate that the uploaded file should be between a minimum and maximum size in kilobytes.
169-
*
170-
* @param string|int $minSize
171-
* @param string|int $maxSize
172-
* @return $this
173201
*/
174-
public function between($minSize, $maxSize)
202+
public function between(string|int $minSize, string|int $maxSize, ?string $units = null): static
175203
{
176-
$this->minimumFileSize = $this->toKilobytes($minSize);
177-
$this->maximumFileSize = $this->toKilobytes($maxSize);
204+
$this->minimumFileSize = $this->toKilobytes($minSize, $this->units($units));
205+
$this->maximumFileSize = $this->toKilobytes($maxSize, $this->units($units));
178206

179207
return $this;
180208
}
181209

182210
/**
183211
* Indicate that the uploaded file should be no less than the given number of kilobytes.
184-
*
185-
* @param string|int $size
186-
* @return $this
187212
*/
188-
public function min($size)
213+
public function min(string|int $size, ?string $units = null): static
189214
{
190-
$this->minimumFileSize = $this->toKilobytes($size);
215+
$this->minimumFileSize = $this->toKilobytes($size, $this->units($units));
191216

192217
return $this;
193218
}
194219

195220
/**
196221
* Indicate that the uploaded file should be no more than the given number of kilobytes.
197-
*
198-
* @param string|int $size
199-
* @return $this
200222
*/
201-
public function max($size)
223+
public function max(string|int $size, ?string $units = null): static
202224
{
203-
$this->maximumFileSize = $this->toKilobytes($size);
225+
$this->maximumFileSize = $this->toKilobytes($size, $this->units($units));
204226

205227
return $this;
206228
}
207229

230+
/**
231+
* Resolve the units to use for size calculations.
232+
*/
233+
protected function units(?string $units = null): string
234+
{
235+
return $units ?? $this->units;
236+
}
237+
208238
/**
209239
* Convert a potentially human-friendly file size to kilobytes.
210-
*
211-
* @param string|int $size
212-
* @return mixed
213240
*/
214-
protected function toKilobytes($size)
241+
protected function toKilobytes(string|int $size, string $units): float|int
215242
{
216243
if (! is_string($size)) {
217244
return $size;
218245
}
219246

220-
$size = strtolower(trim($size));
247+
if (($value = $this->parseSize($size)) === false || $value < 0) {
248+
throw new InvalidArgumentException('Invalid numeric value in file size.');
249+
}
250+
251+
return $units === self::BINARY
252+
? $this->toBinaryKilobytes($size, $value)
253+
: $this->toInternationalKilobytes($size, $value);
254+
}
255+
256+
/**
257+
* Parse the numeric portion from a file size string.
258+
*/
259+
protected function parseSize($size): false|float
260+
{
261+
return filter_var(
262+
is_numeric($size)
263+
? $size
264+
: Str::before(trim($size), Str::match('/[a-zA-Z]/', trim($size))),
265+
FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND
266+
);
267+
}
268+
269+
/**
270+
* Convert a human-friendly file size to kilobytes using the International System.
271+
*/
272+
protected function toInternationalKilobytes(string $size, float $value): float|int
273+
{
274+
return round(
275+
$this->protectValueFromOverflow(
276+
$this->prepareValueForPrecision($value),
277+
! is_numeric($size)
278+
? match (substr(strtolower(trim($size)), -2)) {
279+
'kb' => 1,
280+
'mb' => 1_000,
281+
'gb' => 1_000_000,
282+
'tb' => 1_000_000_000,
283+
default => throw new InvalidArgumentException(
284+
'Invalid file size suffix. Valid suffixes are: KB, MB, GB, TB (case insensitive).'
285+
),
286+
} : 1
287+
)
288+
);
289+
}
290+
291+
/**
292+
* Convert a human-friendly file size to kilobytes using the Binary System.
293+
*/
294+
protected function toBinaryKilobytes(string $size, float $value): float|int
295+
{
296+
return round(
297+
$this->protectValueFromOverflow(
298+
$this->prepareValueForPrecision($value),
299+
! is_numeric($size)
300+
? match (substr(strtolower(trim($size)), -2)) {
301+
'kb' => 1,
302+
'mb' => 1_024,
303+
'gb' => 1_048_576,
304+
'tb' => 1_073_741_824,
305+
default => throw new InvalidArgumentException(
306+
'Invalid file size suffix. Valid suffixes are: KB, MB, GB, TB (case insensitive).'
307+
),
308+
} : 1
309+
)
310+
);
311+
}
221312

222-
$value = floatval($size);
313+
/**
314+
* Converts whole numbers to integers for exact arithmetic while keeping
315+
* fractional numbers as floats; also provides overflow protection by
316+
* falling back to float arithmetic for values too large for integer range.
317+
*/
318+
protected function prepareValueForPrecision(float $value): float|int
319+
{
320+
return $value > PHP_INT_MAX
321+
|| $value < PHP_INT_MIN
322+
|| ((float) (int) $value) !== $value
323+
? $value
324+
: (int) $value;
325+
}
223326

224-
return round(match (true) {
225-
Str::endsWith($size, 'kb') => $value * 1,
226-
Str::endsWith($size, 'mb') => $value * 1_000,
227-
Str::endsWith($size, 'gb') => $value * 1_000_000,
228-
Str::endsWith($size, 'tb') => $value * 1_000_000_000,
229-
default => throw new InvalidArgumentException('Invalid file size suffix.'),
230-
});
327+
/**
328+
* Protect calculations from integer overflow by switching to float arithmetic when necessary.
329+
*/
330+
protected function protectValueFromOverflow(float|int $value, int $multiplier): float|int
331+
{
332+
return $value > PHP_INT_MAX / $multiplier
333+
|| $value < PHP_INT_MIN / $multiplier
334+
|| is_float($value)
335+
? (float) $value * $multiplier
336+
: (int) $value * $multiplier;
231337
}
232338

233339
/**
@@ -283,14 +389,18 @@ protected function buildValidationRules()
283389
$rules[] = 'extensions:'.implode(',', array_map(strtolower(...), $this->allowedExtensions));
284390
}
285391

286-
$rules[] = match (true) {
287-
is_null($this->minimumFileSize) && is_null($this->maximumFileSize) => null,
288-
is_null($this->maximumFileSize) => "min:{$this->minimumFileSize}",
289-
is_null($this->minimumFileSize) => "max:{$this->maximumFileSize}",
290-
$this->minimumFileSize !== $this->maximumFileSize => "between:{$this->minimumFileSize},{$this->maximumFileSize}",
291-
default => "size:{$this->minimumFileSize}",
392+
$rule = match (true) {
393+
$this->minimumFileSize === null && $this->maximumFileSize === null => null,
394+
$this->maximumFileSize === null => "min:{$this->minimumFileSize}",
395+
$this->minimumFileSize === null => "max:{$this->maximumFileSize}",
396+
$this->minimumFileSize === $this->maximumFileSize => "size:{$this->minimumFileSize}",
397+
default => "between:{$this->minimumFileSize},{$this->maximumFileSize}",
292398
};
293399

400+
if ($rule) {
401+
$rules[] = $rule;
402+
}
403+
294404
return array_merge(array_filter($rules), $this->customRules);
295405
}
296406

0 commit comments

Comments
 (0)