@@ -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,166 @@ public function extensions($extensions)
150
165
return $ this ;
151
166
}
152
167
168
+ /**
169
+ * Set the units for size validation to binary (instance method).
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 (instance method).
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
+ protected function units (?string $ units = null ): string
231
+ {
232
+ return $ units ?? $ this ->units ;
233
+ }
234
+
208
235
/**
209
236
* Convert a potentially human-friendly file size to kilobytes.
210
- *
211
- * @param string|int $size
212
- * @return mixed
213
237
*/
214
- protected function toKilobytes ($ size)
238
+ protected function toKilobytes (string | int $ size, string $ units ): float | int
215
239
{
216
240
if (! is_string ($ size )) {
217
241
return $ size ;
218
242
}
219
243
220
- $ size = strtolower (trim ($ size ));
244
+ if (($ value = $ this ->parseSize ($ size )) === false || $ value < 0 ) {
245
+ throw new InvalidArgumentException ('Invalid numeric value in file size. ' );
246
+ }
221
247
222
- $ value = floatval ($ size );
248
+ return $ units === self ::BINARY
249
+ ? $ this ->toBinaryKilobytes ($ size , $ value )
250
+ : $ this ->toInternationalKilobytes ($ size , $ value );
251
+ }
223
252
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
- });
253
+ protected function parseSize ($ size ): false |float
254
+ {
255
+ return filter_var (
256
+ is_numeric ($ size )
257
+ ? $ size
258
+ : Str::before (trim ($ size ), Str::match ('/[a-zA-Z]/ ' , trim ($ size ))),
259
+ FILTER_VALIDATE_FLOAT , FILTER_FLAG_ALLOW_THOUSAND
260
+ );
261
+ }
262
+
263
+ /**
264
+ * Convert a human-friendly file size to kilobytes using the International System.
265
+ */
266
+ protected function toInternationalKilobytes (string $ size , float $ value ): float |int
267
+ {
268
+ return round (
269
+ $ this ->protectValueFromOverflow (
270
+ $ this ->prepareValueForPrecision ($ value ),
271
+ ! is_numeric ($ size )
272
+ ? match (substr (strtolower (trim ($ size )), -2 )) {
273
+ 'kb ' => 1 ,
274
+ 'mb ' => 1_000 ,
275
+ 'gb ' => 1_000_000 ,
276
+ 'tb ' => 1_000_000_000 ,
277
+ default => throw new InvalidArgumentException (
278
+ 'Invalid file size suffix. Valid suffixes are: KB, MB, GB, TB (case insensitive). '
279
+ ),
280
+ } : 1
281
+ )
282
+ );
283
+ }
284
+
285
+ /**
286
+ * Convert a human-friendly file size to kilobytes using the Binary System.
287
+ */
288
+ protected function toBinaryKilobytes (string $ size , float $ value ): float |int
289
+ {
290
+ return round (
291
+ $ this ->protectValueFromOverflow (
292
+ $ this ->prepareValueForPrecision ($ value ),
293
+ ! is_numeric ($ size )
294
+ ? match (substr (strtolower (trim ($ size )), -2 )) {
295
+ 'kb ' => 1 ,
296
+ 'mb ' => 1_024 ,
297
+ 'gb ' => 1_048_576 ,
298
+ 'tb ' => 1_073_741_824 ,
299
+ default => throw new InvalidArgumentException (
300
+ 'Invalid file size suffix. Valid suffixes are: KB, MB, GB, TB (case insensitive). '
301
+ ),
302
+ } : 1
303
+ )
304
+ );
305
+ }
306
+
307
+ /**
308
+ * Converts whole numbers to integers for exact arithmetic while keeping
309
+ * fractional numbers as floats; also provides overflow protection by
310
+ * falling back to float arithmetic for values too large for integer range.
311
+ */
312
+ protected function prepareValueForPrecision (float $ value ): float |int
313
+ {
314
+ return $ value > PHP_INT_MAX
315
+ || $ value < PHP_INT_MIN
316
+ || ((float ) (int ) $ value ) !== $ value
317
+ ? $ value
318
+ : (int ) $ value ;
319
+ }
320
+
321
+ protected function protectValueFromOverflow (float |int $ value , int $ multiplier ): float |int
322
+ {
323
+ return $ value > PHP_INT_MAX / $ multiplier
324
+ || $ value < PHP_INT_MIN / $ multiplier
325
+ || is_float ($ value )
326
+ ? (float ) $ value * $ multiplier
327
+ : (int ) $ value * $ multiplier ;
231
328
}
232
329
233
330
/**
@@ -283,14 +380,18 @@ protected function buildValidationRules()
283
380
$ rules [] = 'extensions: ' .implode (', ' , array_map (strtolower (...), $ this ->allowedExtensions ));
284
381
}
285
382
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 }" ,
383
+ $ rule = match (true ) {
384
+ $ this ->minimumFileSize === null && $ this ->maximumFileSize === null => null ,
385
+ $ this ->maximumFileSize === null => "min: {$ this ->minimumFileSize }" ,
386
+ $ this ->minimumFileSize === null => "max: {$ this ->maximumFileSize }" ,
387
+ $ this ->minimumFileSize === $ this ->maximumFileSize => "size : {$ this ->minimumFileSize }" ,
388
+ default => "between : {$ this ->minimumFileSize } , { $ this -> maximumFileSize }" ,
292
389
};
293
390
391
+ if ($ rule ) {
392
+ $ rules [] = $ rule ;
393
+ }
394
+
294
395
return array_merge (array_filter ($ rules ), $ this ->customRules );
295
396
}
296
397
0 commit comments