aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-16 10:00:50 +0900
committernsfisis <nsfisis@gmail.com>2026-08-16 10:00:50 +0900
commit112df62a01e482f33b68e1dac190f70085fcec64 (patch)
tree19d126032309193cfd5f5d0fb63b6562ef17e5c9 /crates/shirabe-php-shim
parent3fb08fab7ca69a028a9a89544b246ca70534cbce (diff)
downloadphp-shirabe-112df62a01e482f33b68e1dac190f70085fcec64.tar.gz
php-shirabe-112df62a01e482f33b68e1dac190f70085fcec64.tar.zst
php-shirabe-112df62a01e482f33b68e1dac190f70085fcec64.zip
refactor(php-shim): give parse_url a typed UrlComponents result
parse_url now returns Option<UrlComponents> instead of a PhpMixed array, and the component-selecting overload with the PHP_URL_* constants is gone: callers read the field they want. Two call sites change behaviour as a result, both towards PHP: * CurlDownloader::handle_redirect tested scheme and host with is_null(), so an unparsable Location header (PhpMixed::Bool(false)) counted as an absolute URL. PHP's truthiness test sends it to the relative-path branch. * Url::get_origin appended a literal port 0, which PHP treats as falsy and leaves off. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-shim')
-rw-r--r--crates/shirabe-php-shim/src/stream.rs6
-rw-r--r--crates/shirabe-php-shim/src/url.rs94
2 files changed, 30 insertions, 70 deletions
diff --git a/crates/shirabe-php-shim/src/stream.rs b/crates/shirabe-php-shim/src/stream.rs
index bd5e10af..216c68d4 100644
--- a/crates/shirabe-php-shim/src/stream.rs
+++ b/crates/shirabe-php-shim/src/stream.rs
@@ -84,9 +84,9 @@ pub fn stream_isatty(stream: PhpResource) -> bool {
/// `STREAM_IS_URL`; this classifies by the scheme itself, so a registered custom wrapper claiming to
/// be local (or vice versa) comes out differently than in PHP.
pub fn stream_is_local(path: &str) -> bool {
- match crate::parse_url(path, crate::PHP_URL_SCHEME) {
- PhpMixed::String(scheme) => scheme.eq_ignore_ascii_case("file"),
- _ => true,
+ match crate::parse_url(path).and_then(|url| url.scheme) {
+ Some(scheme) => scheme.eq_ignore_ascii_case("file"),
+ None => true,
}
}
diff --git a/crates/shirabe-php-shim/src/url.rs b/crates/shirabe-php-shim/src/url.rs
index c2ac725d..45920402 100644
--- a/crates/shirabe-php-shim/src/url.rs
+++ b/crates/shirabe-php-shim/src/url.rs
@@ -1,80 +1,40 @@
use crate::PhpMixed;
use indexmap::IndexMap;
-pub const PHP_URL_SCHEME: i64 = 0;
-pub const PHP_URL_HOST: i64 = 1;
-pub const PHP_URL_PORT: i64 = 2;
-pub const PHP_URL_USER: i64 = 3;
-pub const PHP_URL_PASS: i64 = 4;
-pub const PHP_URL_PATH: i64 = 5;
-pub const PHP_URL_QUERY: i64 = 6;
-pub const PHP_URL_FRAGMENT: i64 = 7;
-
-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)
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+pub struct UrlComponents {
+ pub scheme: Option<String>,
+ pub host: Option<String>,
+ pub port: Option<i64>,
+ pub user: Option<String>,
+ pub pass: Option<String>,
+ pub path: Option<String>,
+ pub query: Option<String>,
+ pub fragment: Option<String>,
}
-pub fn parse_url_all(url: &str) -> PhpMixed {
+/// PHP `parse_url()`. `None` for an invalid URL.
+pub fn parse_url(url: &str) -> Option<UrlComponents> {
// TODO(php-semantics): 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)
+ let parsed = reqwest::Url::parse(url).ok()?;
+ Some(UrlComponents {
+ scheme: Some(parsed.scheme().to_string()),
+ host: parsed.host_str().map(str::to_string),
+ port: parsed.port().map(i64::from),
+ user: Some(parsed.username())
+ .filter(|user| !user.is_empty())
+ .map(str::to_string),
+ pass: parsed.password().map(str::to_string),
+ path: Some(parsed.path())
+ .filter(|path| !path.is_empty())
+ .map(str::to_string),
+ query: parsed.query().map(str::to_string),
+ fragment: parsed.fragment().map(str::to_string),
+ })
}
pub fn http_build_query_mixed(