Skip to content

Commit 4bfce6c

Browse files
committed
fix(string): validate only URL-safe characters in Base64Variant::UrlSafe
- Restrict UrlSafe regex to [A-Za-z0-9\-_] instead of accepting standard +/ chars - Add test asserting standard Base64 is rejected in UrlSafe mode - Update string-validation guide, enum docblock, and changelog - Remove resolved bug from ISSUES.md and renumber sections
1 parent 3d4788f commit 4bfce6c

6 files changed

Lines changed: 49 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file.
1313
- `satisfies()`, `satisfiesAll()`, `satisfiesAny()`, `satisfiesNone()`, and `ArrayValidator::contains()` now clone any `FieldValidator` operand at capture time and rebuild those operands when the outer validator is cloned, so later mutations or clone-local pipeline state do not leak across validator instances
1414
- `FieldValidator::tryValidate()` now resets transient type-tracking before and after every validation run, so validators that use `transform()`/`pipe()` remain repeatable across multiple calls and clones do not inherit stale runtime state
1515
- `default()` no longer deep-copies object values; object defaults are returned as-is (shared by handle). Use `defaultUsing()` when each validation run needs a fresh object instance
16+
- `Base64Variant::UrlSafe` now strictly validates URL-safe characters (`-_`) only; previously it was identical to `Base64Variant::Any` (accepted both standard and URL-safe)
1617

1718
### Added
1819

ISSUES.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Project Issues and Suggestions
2+
3+
This document tracks identified bugs, architectural inconsistencies, and potential improvements for the Lemmon Validator library.
4+
5+
## 1. Architectural Suggestions
6+
7+
### IntValidator Coercion Precision
8+
9+
`IntValidator` uses `is_numeric()` during coercion, which means a string like `"1.5"` is successfully coerced (truncated) to `1`. While "form-safe," this truncation can hide data precision issues. Consider providing a way to enforce strict integer strings or documenting this behavior clearly.
10+
11+
### UnitEnum Support
12+
13+
The `enum()` validator currently only supports `BackedEnum`. Adding support for basic `UnitEnum` (cases only) would increase utility for projects that do not use backed values for all enums.
14+
15+
### Mixed Type Coercion in Combinators
16+
17+
The static combinators `Validator::anyOf()`, `allOf()`, and `not()` return a "mixed" validator that does not support top-level coercion. Users expecting coercion must enable it on individual sub-validators.
18+
19+
---
20+
21+
## 2. Feature Suggestions
22+
23+
### Built-in String Sanitizers
24+
25+
Common operations like `trim()`, `lowercase()`, and `uppercase()` are currently implemented via `pipe('trim')`. Adding them as first-class methods (e.g., `->trim()`) would improve discoverability and follow the fluent API pattern of other validators.
26+
27+
### Explicit nullable() Method
28+
29+
Although fields are optional (allow null) by default, adding an explicit `->nullable()` method (as an alias for default behavior) can improve code readability and intent, especially when contrasted with `->required()`.
30+
31+
### Instance Validation
32+
33+
Add a validator for checking object instances (e.g., `Validator::isInstance(MyClass::class)`).
34+
35+
### Error Message Templates
36+
37+
Implement a more flexible way to configure global error message templates for core constraints (like `minLength` or `between`) to support localization and consistent branding across applications.

docs/guides/string-validation.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,10 @@ $base64 = $standardValidator->validate('SGVsbG8gV29ybGQ='); // "Hello World"
183183
$base64 = $standardValidator->validate('dGVzdA=='); // "test"
184184
$base64 = $standardValidator->validate('YWJj'); // "abc" (no padding)
185185

186-
// URL-safe Base64 (uses -, _, and accepts both variants)
186+
// URL-safe Base64 (uses - and _ instead of + and /, padding optional)
187187
$urlSafeValidator = Validator::isString()->base64(Base64Variant::UrlSafe);
188-
$base64 = $urlSafeValidator->validate('SGVsbG8gV29ybGQ'); // URL-safe format
189-
$base64 = $urlSafeValidator->validate('SGVsbG8gV29ybGQ='); // Standard format also accepted
188+
$base64 = $urlSafeValidator->validate('SGVsbG8gV29ybGQ'); // URL-safe format (no padding)
189+
$base64 = $urlSafeValidator->validate('PDw_Pz4-'); // URL-safe chars - and _
190190

191191
// Any variant (accepts both standard and URL-safe)
192192
$anyValidator = Validator::isString()->base64(Base64Variant::Any);

src/Lemmon/Validator/Base64Variant.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ enum Base64Variant: string
1616
case Standard = 'standard';
1717

1818
/**
19-
* URL-safe Base64 encoding (uses -, _, and no padding).
20-
* URL-safe parsers typically accept both standard and URL-safe variants.
19+
* URL-safe Base64 encoding (uses - and _ instead of + and /, padding optional).
2120
*/
2221
case UrlSafe = 'urlsafe';
2322

src/Lemmon/Validator/StringValidator.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,7 @@ public function base64(
222222
Base64Variant::Standard => preg_match('/^[A-Za-z0-9+\/]*={0,2}$/', $value) === 1
223223
&& ($decoded = base64_decode($value, true)) !== false
224224
&& base64_encode($decoded) === $value,
225-
Base64Variant::UrlSafe => preg_match('/^[A-Za-z0-9+\/]*={0,2}$/', $value) === 1 // Try standard Base64 first // URL-safe parsers accept both standard and URL-safe variants
226-
&& ($decoded = base64_decode($value, true)) !== false
227-
&& base64_encode($decoded) === $value
228-
// Or try URL-safe Base64
229-
|| preg_match('/^[A-Za-z0-9\-_]*={0,2}$/', $value) === 1
225+
Base64Variant::UrlSafe => preg_match('/^[A-Za-z0-9\-_]*={0,2}$/', $value) === 1
230226
&& base64_decode(strtr($value, '-_', '+/'), true) !== false,
231227
Base64Variant::Any => preg_match('/^[A-Za-z0-9+\/]*={0,2}$/', $value) === 1 // Try standard Base64 first
232228
&& ($decoded = base64_decode($value, true)) !== false

tests/StringValidatorTest.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,19 +228,20 @@
228228
it('should validate URL-safe base64 strings', function () {
229229
$urlSafeValidator = Validator::isString()->base64(Base64Variant::UrlSafe);
230230

231-
// Valid URL-safe base64 strings (no padding, uses - and _)
232231
expect($urlSafeValidator->validate('SGVsbG8gV29ybGQ'))->toBe('SGVsbG8gV29ybGQ'); // "Hello World" (no padding)
233232
expect($urlSafeValidator->validate('dGVzdA'))->toBe('dGVzdA'); // "test" (no padding)
234233
expect($urlSafeValidator->validate('YWJj'))->toBe('YWJj'); // "abc"
235-
236-
// URL-safe parsers accept both standard and URL-safe variants
237-
// So standard Base64 with + and / should also be accepted
238-
expect($urlSafeValidator->validate('SGVsbG8gV29ybGQ='))->toBe('SGVsbG8gV29ybGQ='); // Standard Base64 accepted
234+
expect($urlSafeValidator->validate('PDw_Pz4-'))->toBe('PDw_Pz4-'); // contains - and _ chars
239235

240236
// Invalid: contains invalid characters
241237
$urlSafeValidator->validate('SGVsbG8gV29ybGQ!');
242238
})->throws(ValidationException::class, 'Value must be a valid URL-safe Base64 encoded string');
243239

240+
it('should reject standard base64 characters in URL-safe mode', function () {
241+
$urlSafeValidator = Validator::isString()->base64(Base64Variant::UrlSafe);
242+
$urlSafeValidator->validate('PDw/Pz4+'); // standard Base64 with + and /
243+
})->throws(ValidationException::class, 'Value must be a valid URL-safe Base64 encoded string');
244+
244245
it('should validate base64 with Any variant', function () {
245246
$anyValidator = Validator::isString()->base64(Base64Variant::Any);
246247

0 commit comments

Comments
 (0)