aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-21 17:46:18 +0900
committernsfisis <nsfisis@gmail.com>2026-06-21 18:24:40 +0900
commitf8b402e0265bae355cb72be8dd1a592ee5bb2edb (patch)
tree95059704c1acbe67bdcce0500c3258c18b27fa7d /crates
parentf05e1a55cf0d44e9611e06a3d7b4fccdcb90ce7b (diff)
downloadphp-shirabe-f8b402e0265bae355cb72be8dd1a592ee5bb2edb.tar.gz
php-shirabe-f8b402e0265bae355cb72be8dd1a592ee5bb2edb.tar.zst
php-shirabe-f8b402e0265bae355cb72be8dd1a592ee5bb2edb.zip
feat(php-shim): implement filter_var email/ip/int validators
Replace the todo!() bodies of filter_var_email, filter_var_ip, and filter_var_int_with_range with working validators. Like filter_var_url these are pragmatic rather than byte-for-byte compatible with PHP's hand-written filters; the divergences are noted in comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-php-shim/src/filter.rs90
1 files changed, 84 insertions, 6 deletions
diff --git a/crates/shirabe-php-shim/src/filter.rs b/crates/shirabe-php-shim/src/filter.rs
index bd1a6fa..bbd9a56 100644
--- a/crates/shirabe-php-shim/src/filter.rs
+++ b/crates/shirabe-php-shim/src/filter.rs
@@ -1,3 +1,4 @@
+// TODO(phase-c):
// Without FILTER_NULL_ON_FAILURE, php_filter_boolean trims surrounding
// whitespace, lowercases, and yields true only for "1"/"true"/"on"/"yes";
// every other input (including the "0"/"false"/"off"/"no"/"" set) yields
@@ -18,14 +19,91 @@ pub fn filter_var_url(value: &str) -> bool {
reqwest::Url::parse(value).is_ok()
}
-pub fn filter_var_email(_value: &str) -> bool {
- todo!()
+// TODO(phase-c):
+// PHP's FILTER_VALIDATE_EMAIL applies a long PCRE with length lookaheads,
+// quoted local parts, and bracketed IP-literal domains, which the `regex` crate
+// cannot express. This is a simplified validator covering the common
+// `local@domain` shape plus the RFC length limits PHP enforces; it diverges
+// from PHP by rejecting quoted local parts (e.g. `"a b"@x.com`) and `[IPv6:...]`
+// literal domains.
+pub fn filter_var_email(value: &str) -> bool {
+ let Some(at) = value.rfind('@') else {
+ return false;
+ };
+ let local = &value[..at];
+ let domain = &value[at + 1..];
+
+ if local.is_empty() || local.len() > 64 {
+ return false;
+ }
+ if domain.is_empty() || domain.len() > 255 {
+ return false;
+ }
+
+ is_valid_email_local(local) && is_valid_email_domain(domain)
+}
+
+// Reject leading/trailing/consecutive dots; otherwise allow printable ASCII
+// except the specials PHP excludes from the local part.
+fn is_valid_email_local(local: &str) -> bool {
+ if local.starts_with('.') || local.ends_with('.') || local.contains("..") {
+ return false;
+ }
+ local
+ .bytes()
+ .all(|b| b > 0x20 && b < 0x7F && !matches!(b, b'@' | b'"' | b'[' | b']' | b'\\'))
+}
+
+// One or more dot-separated labels of [A-Za-z0-9-], each non-empty and not
+// starting or ending with a hyphen.
+fn is_valid_email_domain(domain: &str) -> bool {
+ domain.split('.').all(|label| {
+ !label.is_empty()
+ && !label.starts_with('-')
+ && !label.ends_with('-')
+ && label
+ .bytes()
+ .all(|b| b.is_ascii_alphanumeric() || b == b'-')
+ })
+}
+
+// TODO(phase-c):
+// PHP's FILTER_VALIDATE_IP accepts both IPv4 and IPv6 literals. Rust's IpAddr
+// parser is a close match (both reject leading zeros in IPv4 octets), but is not
+// guaranteed byte-for-byte identical to PHP's hand-written validator on exotic
+// IPv6 forms.
+pub fn filter_var_ip(value: &str) -> bool {
+ value.parse::<std::net::IpAddr>().is_ok()
}
-pub fn filter_var_ip(_value: &str) -> bool {
- todo!()
+// TODO(phase-c):
+// Mirrors PHP's FILTER_VALIDATE_INT with min_range/max_range: surrounding
+// whitespace is trimmed, an optional sign is allowed, leading zeros are rejected
+// (except a lone "0"), and the parsed value must fall within [min, max]
+// inclusive.
+pub fn filter_var_int_with_range(value: &str, min: i64, max: i64) -> bool {
+ match parse_filter_int(value) {
+ Some(n) => min <= n && n <= max,
+ None => false,
+ }
}
-pub fn filter_var_int_with_range(_value: &str, _min: i64, _max: i64) -> bool {
- todo!()
+fn parse_filter_int(value: &str) -> Option<i64> {
+ let s = value.trim_matches([' ', '\t', '\n', '\r', '\x0B', '\x0C']);
+ let (sign, digits) = match s.strip_prefix('-') {
+ Some(rest) => (-1i64, rest),
+ None => (1i64, s.strip_prefix('+').unwrap_or(s)),
+ };
+ if digits.is_empty() {
+ return None;
+ }
+ // A lone zero is valid; any other number must not have a leading zero.
+ if digits != "0" && digits.starts_with('0') {
+ return None;
+ }
+ if !digits.bytes().all(|b| b.is_ascii_digit()) {
+ return None;
+ }
+ let magnitude: i64 = digits.parse().ok()?;
+ Some(sign * magnitude)
}