@@ -17,6 +17,16 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule
17
17
{
18
18
use Conditionable, Macroable;
19
19
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
+
20
30
/**
21
31
* The MIME types that the given file should match. This array may also contain file extensions.
22
32
*
@@ -45,6 +55,11 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule
45
55
*/
46
56
protected $ maximumFileSize = null ;
47
57
58
+ /**
59
+ * The units used for size validation.
60
+ */
61
+ protected string $ units = self ::INTERNATIONAL ;
62
+
48
63
/**
49
64
* An array of custom rules that will be merged into the validation rules.
50
65
*
@@ -150,84 +165,175 @@ public function extensions($extensions)
150
165
return $ this ;
151
166
}
152
167
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
+
153
188
/**
154
189
* Indicate that the uploaded file should be exactly a certain size in kilobytes.
155
- *
156
- * @param string|int $size
157
- * @return $this
158
190
*/
159
- public function size ($ size)
191
+ public function size (string | int $ size, ? string $ units = null ): static
160
192
{
161
- $ this ->minimumFileSize = $ this ->toKilobytes ($ size );
193
+ $ this ->minimumFileSize = $ this ->toKilobytes ($ size, $ this -> units ( $ units ) );
162
194
$ this ->maximumFileSize = $ this ->minimumFileSize ;
163
195
164
196
return $ this ;
165
197
}
166
198
167
199
/**
168
200
* 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
173
201
*/
174
- public function between ($ minSize , $ maxSize)
202
+ public function between (string | int $ minSize , string | int $ maxSize, ? string $ units = null ): static
175
203
{
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 ) );
178
206
179
207
return $ this ;
180
208
}
181
209
182
210
/**
183
211
* Indicate that the uploaded file should be no less than the given number of kilobytes.
184
- *
185
- * @param string|int $size
186
- * @return $this
187
212
*/
188
- public function min ($ size)
213
+ public function min (string | int $ size, ? string $ units = null ): static
189
214
{
190
- $ this ->minimumFileSize = $ this ->toKilobytes ($ size );
215
+ $ this ->minimumFileSize = $ this ->toKilobytes ($ size, $ this -> units ( $ units ) );
191
216
192
217
return $ this ;
193
218
}
194
219
195
220
/**
196
221
* Indicate that the uploaded file should be no more than the given number of kilobytes.
197
- *
198
- * @param string|int $size
199
- * @return $this
200
222
*/
201
- public function max ($ size)
223
+ public function max (string | int $ size, ? string $ units = null ): static
202
224
{
203
- $ this ->maximumFileSize = $ this ->toKilobytes ($ size );
225
+ $ this ->maximumFileSize = $ this ->toKilobytes ($ size, $ this -> units ( $ units ) );
204
226
205
227
return $ this ;
206
228
}
207
229
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
+
208
238
/**
209
239
* Convert a potentially human-friendly file size to kilobytes.
210
- *
211
- * @param string|int $size
212
- * @return mixed
213
240
*/
214
- protected function toKilobytes ($ size)
241
+ protected function toKilobytes (string | int $ size, string $ units ): float | int
215
242
{
216
243
if (! is_string ($ size )) {
217
244
return $ size ;
218
245
}
219
246
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
+ }
221
312
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
+ }
223
326
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 ;
231
337
}
232
338
233
339
/**
@@ -283,14 +389,18 @@ protected function buildValidationRules()
283
389
$ rules [] = 'extensions: ' .implode (', ' , array_map (strtolower (...), $ this ->allowedExtensions ));
284
390
}
285
391
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 }" ,
292
398
};
293
399
400
+ if ($ rule ) {
401
+ $ rules [] = $ rule ;
402
+ }
403
+
294
404
return array_merge (array_filter ($ rules ), $ this ->customRules );
295
405
}
296
406
0 commit comments