aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-20 22:11:59 +0900
committernsfisis <nsfisis@gmail.com>2026-06-20 22:25:15 +0900
commit8538a375a4ddcfad3fabc8d19ca41c148136b63b (patch)
treee5b93b856be1ae8cddb4973484f94d03731c246e /crates/shirabe-php-shim
parent6a5484cbf8e75c99098e5fba98a1c5e4c1b7c2f2 (diff)
downloadphp-shirabe-8538a375a4ddcfad3fabc8d19ca41c148136b63b.tar.gz
php-shirabe-8538a375a4ddcfad3fabc8d19ca41c148136b63b.tar.zst
php-shirabe-8538a375a4ddcfad3fabc8d19ca41c148136b63b.zip
feat(php-shim): implement parse_url and filter_var URL/boolean filters
Back parse_url/parse_url_all with reqwest::Url and add the FILTER_VALIDATE_URL and FILTER_VALIDATE_BOOLEAN arms to filter_var, so Config::prohibit_url_by_config no longer hits todo!(). reqwest::Url is not a byte-for-byte port of PHP's parser; the divergences are noted with TODO(phase-c). Switch the scheme in_array in prohibit_url_by_config to strict: the needle is a string-or-null and the haystack holds only non-numeric string literals, so loose and strict comparison are provably equivalent here, avoiding a dependency on PHP loose `==` semantics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-shim')
-rw-r--r--crates/shirabe-php-shim/Cargo.toml1
-rw-r--r--crates/shirabe-php-shim/src/lib.rs89
2 files changed, 84 insertions, 6 deletions
diff --git a/crates/shirabe-php-shim/Cargo.toml b/crates/shirabe-php-shim/Cargo.toml
index abfe89e..797191e 100644
--- a/crates/shirabe-php-shim/Cargo.toml
+++ b/crates/shirabe-php-shim/Cargo.toml
@@ -9,6 +9,7 @@ chrono.workspace = true
fastrand.workspace = true
indexmap.workspace = true
regex.workspace = true
+reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
zip.workspace = true
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs
index d47a14a..4bb309e 100644
--- a/crates/shirabe-php-shim/src/lib.rs
+++ b/crates/shirabe-php-shim/src/lib.rs
@@ -750,12 +750,71 @@ pub fn fclose(_file: PhpMixed) {
todo!()
}
-pub fn parse_url(_url: &str, _component: i64) -> PhpMixed {
- todo!()
+pub fn parse_url(url: &str, component: i64) -> PhpMixed {
+ let all = parse_url_all(url);
+ let map = match all.as_array() {
+ Some(map) => map,
+ // parse_url_all already collapsed a malformed URL to false; propagate it.
+ None => return all,
+ };
+ let key = match component {
+ PHP_URL_SCHEME => "scheme",
+ PHP_URL_HOST => "host",
+ PHP_URL_PORT => "port",
+ PHP_URL_USER => "user",
+ PHP_URL_PASS => "pass",
+ PHP_URL_PATH => "path",
+ PHP_URL_QUERY => "query",
+ PHP_URL_FRAGMENT => "fragment",
+ _ => return PhpMixed::Null,
+ };
+ map.get(key).cloned().unwrap_or(PhpMixed::Null)
}
-pub fn parse_url_all(_url: &str) -> PhpMixed {
- todo!()
+pub fn parse_url_all(url: &str) -> PhpMixed {
+ // TODO(phase-c): PHP's parse_url uses php_url_parse_ex, which accepts relative
+ // and partial URLs and leaves an absent component absent. reqwest::Url
+ // (WHATWG/RFC 3986) requires an absolute URL, lowercases the host of special
+ // schemes, and normalizes the path (e.g. "http://host" yields path "/"). This
+ // is therefore not a byte-for-byte compatible port of parse_url.
+ let parsed = match reqwest::Url::parse(url) {
+ Ok(parsed) => parsed,
+ Err(_) => return PhpMixed::Bool(false),
+ };
+ let mut map: IndexMap<String, PhpMixed> = IndexMap::new();
+ map.insert(
+ "scheme".to_string(),
+ PhpMixed::String(parsed.scheme().to_string()),
+ );
+ if let Some(host) = parsed.host_str() {
+ map.insert("host".to_string(), PhpMixed::String(host.to_string()));
+ }
+ if let Some(port) = parsed.port() {
+ map.insert("port".to_string(), PhpMixed::Int(port as i64));
+ }
+ if !parsed.username().is_empty() {
+ map.insert(
+ "user".to_string(),
+ PhpMixed::String(parsed.username().to_string()),
+ );
+ }
+ if let Some(pass) = parsed.password() {
+ map.insert("pass".to_string(), PhpMixed::String(pass.to_string()));
+ }
+ let path = parsed.path();
+ if !path.is_empty() {
+ map.insert("path".to_string(), PhpMixed::String(path.to_string()));
+ }
+ if let Some(query) = parsed.query() {
+ map.insert("query".to_string(), PhpMixed::String(query.to_string()));
+ }
+ if let Some(fragment) = parsed.fragment() {
+ map.insert(
+ "fragment".to_string(),
+ PhpMixed::String(fragment.to_string()),
+ );
+ }
+ PhpMixed::Array(map)
}
pub fn pathinfo(path: PhpMixed, option: i64) -> PhpMixed {
@@ -1238,8 +1297,26 @@ pub const FILTER_VALIDATE_URL: i64 = 273;
pub const FILTER_VALIDATE_IP: i64 = 275;
pub const FILTER_VALIDATE_INT: i64 = 257;
-pub fn filter_var(_value: &str, _filter: i64) -> bool {
- todo!()
+pub fn filter_var(value: &str, filter: i64) -> bool {
+ match filter {
+ // 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
+ // false.
+ FILTER_VALIDATE_BOOLEAN => {
+ let trimmed = value.trim_matches([' ', '\t', '\n', '\r', '\0', '\x0B']);
+ matches!(
+ trimmed.to_ascii_lowercase().as_str(),
+ "1" | "true" | "on" | "yes"
+ )
+ }
+ // TODO(phase-c): PHP's FILTER_VALIDATE_URL parses with php_url_parse_ex and
+ // additionally validates the host as a domain/IPv6 literal. reqwest::Url
+ // (WHATWG/RFC 3986) is stricter on some inputs and more lenient on others,
+ // so this is not a byte-for-byte compatible validator.
+ FILTER_VALIDATE_URL => reqwest::Url::parse(value).is_ok(),
+ _ => todo!(),
+ }
}
// Models the configuration of a standard PHP CLI environment. Settings belonging