Skip to content

Commit 287d154

Browse files
committed
feat: binary file size validation support
1 parent bac0903 commit 287d154

File tree

2 files changed

+648
-34
lines changed

2 files changed

+648
-34
lines changed

src/Illuminate/Validation/Rules/File.php

Lines changed: 100 additions & 34 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 = null;
62+
4863
/**
4964
* An array of custom rules that will be merged into the validation rules.
5065
*
@@ -150,86 +165,133 @@ public function extensions($extensions)
150165
return $this;
151166
}
152167

168+
/**
169+
* Set the default units for size validation to binary.
170+
*/
171+
public static function binary(): static
172+
{
173+
return tap(new static, static fn ($file) => $file->units = self::BINARY);
174+
}
175+
176+
/**
177+
* Set the default units for size validation to international.
178+
*/
179+
public static function international(): static
180+
{
181+
return tap(new static, static fn ($file) => $file->units = self::INTERNATIONAL);
182+
}
183+
153184
/**
154185
* Indicate that the uploaded file should be exactly a certain size in kilobytes.
155-
*
156-
* @param string|int $size
157-
* @return $this
158186
*/
159-
public function size($size)
187+
public function size(string|int $size, ?string $units = null): static
160188
{
161-
$this->minimumFileSize = $this->toKilobytes($size);
189+
$this->minimumFileSize = $this->toKilobytes($size, $this->units($units));
162190
$this->maximumFileSize = $this->minimumFileSize;
163191

164192
return $this;
165193
}
166194

167195
/**
168196
* 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
173197
*/
174-
public function between($minSize, $maxSize)
198+
public function between(string|int $minSize, string|int $maxSize, ?string $units = null): static
175199
{
176-
$this->minimumFileSize = $this->toKilobytes($minSize);
177-
$this->maximumFileSize = $this->toKilobytes($maxSize);
200+
$this->minimumFileSize = $this->toKilobytes($minSize, $this->units($units));
201+
$this->maximumFileSize = $this->toKilobytes($maxSize, $this->units($units));
178202

179203
return $this;
180204
}
181205

182206
/**
183207
* Indicate that the uploaded file should be no less than the given number of kilobytes.
184-
*
185-
* @param string|int $size
186-
* @return $this
187208
*/
188-
public function min($size)
209+
public function min(string|int $size, ?string $units = null): static
189210
{
190-
$this->minimumFileSize = $this->toKilobytes($size);
211+
$this->minimumFileSize = $this->toKilobytes($size, $this->units($units));
191212

192213
return $this;
193214
}
194215

195216
/**
196217
* Indicate that the uploaded file should be no more than the given number of kilobytes.
197-
*
198-
* @param string|int $size
199-
* @return $this
200218
*/
201-
public function max($size)
219+
public function max(string|int $size, ?string $units = null): static
202220
{
203-
$this->maximumFileSize = $this->toKilobytes($size);
221+
$this->maximumFileSize = $this->toKilobytes($size, $this->units($units));
204222

205223
return $this;
206224
}
207225

226+
protected function units(?string $units = null): string
227+
{
228+
return $units ?? $this->units ?? static::INTERNATIONAL;
229+
}
230+
231+
/**
232+
* Converts whole numbers to integers for exact arithmetic while keeping
233+
* fractional numbers as floats; also provides overflow protection by
234+
* falling back to float arithmetic for values too large for integer range.
235+
*/
236+
protected function prepareValueForPrecision(float $value): float|int
237+
{
238+
return $value > PHP_INT_MAX || $value < PHP_INT_MIN || ((float) (int) $value) !== $value
239+
? $value
240+
: (int) $value;
241+
}
242+
208243
/**
209244
* Convert a potentially human-friendly file size to kilobytes.
210-
*
211-
* @param string|int $size
212-
* @return mixed
213245
*/
214-
protected function toKilobytes($size)
246+
protected function toKilobytes(string|int $size, string $units): float|int
215247
{
216248
if (! is_string($size)) {
217249
return $size;
218250
}
219251

220252
$size = strtolower(trim($size));
221253

222-
$value = floatval($size);
254+
$value = (float) $size;
223255

256+
return $units === self::BINARY
257+
? $this->toBinaryKilobytes($size, $value)
258+
: $this->toInternationalKilobytes($size, $value);
259+
}
260+
261+
/**
262+
* Convert a human-friendly file size to kilobytes using the International System.
263+
*/
264+
protected function toInternationalKilobytes(string $size, float $value): float|int
265+
{
266+
$value = $this->prepareValueForPrecision($value);
267+
224268
return round(match (true) {
225-
Str::endsWith($size, 'kb') => $value * 1,
269+
is_numeric($size) => $value,
270+
Str::endsWith($size, 'kb') => $value,
226271
Str::endsWith($size, 'mb') => $value * 1_000,
227272
Str::endsWith($size, 'gb') => $value * 1_000_000,
228273
Str::endsWith($size, 'tb') => $value * 1_000_000_000,
229274
default => throw new InvalidArgumentException('Invalid file size suffix.'),
230275
});
231276
}
232277

278+
/**
279+
* Convert a human-friendly file size to kilobytes using the Binary System.
280+
*/
281+
protected function toBinaryKilobytes(string $size, float $value): float|int
282+
{
283+
$value = $this->prepareValueForPrecision($value);
284+
285+
return round(match (true) {
286+
is_numeric($size) => $value,
287+
Str::endsWith($size, 'kb') => $value,
288+
Str::endsWith($size, 'mb') => $value * 1_024,
289+
Str::endsWith($size, 'gb') => $value * 1_048_576,
290+
Str::endsWith($size, 'tb') => $value * 1_073_741_824,
291+
default => throw new InvalidArgumentException('Invalid file size suffix.'),
292+
});
293+
}
294+
233295
/**
234296
* Specify additional validation rules that should be merged with the default rules during validation.
235297
*
@@ -283,14 +345,18 @@ protected function buildValidationRules()
283345
$rules[] = 'extensions:'.implode(',', array_map(strtolower(...), $this->allowedExtensions));
284346
}
285347

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}",
348+
$rule = match (true) {
349+
$this->minimumFileSize === null && $this->maximumFileSize === null => null,
350+
$this->maximumFileSize === null => "min:{$this->minimumFileSize}",
351+
$this->minimumFileSize === null => "max:{$this->maximumFileSize}",
352+
$this->minimumFileSize === $this->maximumFileSize => "size:{$this->minimumFileSize}",
353+
default => "between:{$this->minimumFileSize},{$this->maximumFileSize}",
292354
};
293355

356+
if ($rule) {
357+
$rules[] = $rule;
358+
}
359+
294360
return array_merge(array_filter($rules), $this->customRules);
295361
}
296362

0 commit comments

Comments
 (0)