1
//! URL parsing module for C API
2
//!
3
//! Provides a C-compatible URL type based on the `url` crate.
4
//! Key types: [`Url`], [`UrlParseError`], [`ResultUrlUrlParseError`].
5
//! Re-exported from `layout/src/lib.rs`.
6

            
7
use alloc::string::String;
8
use core::fmt;
9
use azul_css::{AzString, impl_result, impl_result_inner};
10

            
11
/// A parsed URL
12
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13
#[repr(C)]
14
pub struct Url {
15
    /// The full URL string
16
    pub href: AzString,
17
    /// The scheme (e.g., "https")
18
    pub scheme: AzString,
19
    /// The host (e.g., "example.com")
20
    pub host: AzString,
21
    /// The port number, or 0 if not specified (sentinel value; see `effective_port()`)
22
    pub port: u16,
23
    /// The path (e.g., "/path/to/resource")
24
    pub path: AzString,
25
    /// The query string without '?' (e.g., "key=value")
26
    pub query: AzString,
27
    /// The fragment without '#' (e.g., "section")
28
    pub fragment: AzString,
29
}
30

            
31
/// Error when parsing a URL
32
#[derive(Debug, Clone, PartialEq)]
33
#[repr(C)]
34
pub struct UrlParseError {
35
    /// Error message
36
    pub message: AzString,
37
}
38

            
39
impl fmt::Display for UrlParseError {
40
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41
        write!(f, "{}", self.message.as_str())
42
    }
43
}
44

            
45
#[cfg(feature = "std")]
46
impl std::error::Error for UrlParseError {}
47

            
48
// FFI-safe Result type for URL parsing
49
impl_result!(
50
    Url,
51
    UrlParseError,
52
    ResultUrlUrlParseError,
53
    copy = false,
54
    [Debug, Clone, PartialEq]
55
);
56

            
57
impl Url {
58
    /// Parse a URL from a string
59
    #[cfg(feature = "http")]
60
    pub fn parse(s: &str) -> Result<Self, UrlParseError> {
61
        use ::url::Url as UrlParser;
62
        
63
        let parsed = UrlParser::parse(s)
64
            .map_err(|e| UrlParseError {
65
                message: AzString::from(e.to_string()),
66
            })?;
67
        
68
        Ok(Self {
69
            href: AzString::from(parsed.as_str().to_string()),
70
            scheme: AzString::from(parsed.scheme().to_string()),
71
            host: AzString::from(parsed.host_str().unwrap_or("").to_string()),
72
            port: parsed.port().unwrap_or(0),
73
            path: AzString::from(parsed.path().to_string()),
74
            query: AzString::from(parsed.query().unwrap_or("").to_string()),
75
            fragment: AzString::from(parsed.fragment().unwrap_or("").to_string()),
76
        })
77
    }
78
    
79
    /// Create a URL from components
80
1
    pub fn from_parts(
81
1
        scheme: &str,
82
1
        host: &str,
83
1
        port: u16,
84
1
        path: &str,
85
1
    ) -> Self {
86
1
        let port_str = if port == 0 || (scheme == "http" && port == 80) || (scheme == "https" && port == 443) {
87
1
            String::new()
88
        } else {
89
            format!(":{}", port)
90
        };
91
        
92
1
        let href = format!("{}://{}{}{}", scheme, host, port_str, path);
93
        
94
1
        Self {
95
1
            href: AzString::from(href),
96
1
            scheme: AzString::from(scheme.to_string()),
97
1
            host: AzString::from(host.to_string()),
98
1
            port,
99
1
            path: AzString::from(path.to_string()),
100
1
            query: AzString::from(String::new()),
101
1
            fragment: AzString::from(String::new()),
102
1
        }
103
1
    }
104
    
105
    /// Get the full URL as a string slice
106
    pub fn as_str(&self) -> &str {
107
        self.href.as_str()
108
    }
109
    
110
    /// Check if this is an HTTPS URL
111
1
    pub fn is_https(&self) -> bool {
112
1
        self.scheme.as_str() == "https"
113
1
    }
114
    
115
    /// Check if this is an HTTP URL
116
    pub fn is_http(&self) -> bool {
117
        self.scheme.as_str() == "http"
118
    }
119
    
120
    /// Get the effective port (using default ports for http/https)
121
1
    pub fn effective_port(&self) -> u16 {
122
1
        if self.port != 0 {
123
1
            self.port
124
        } else if self.is_https() {
125
            443
126
        } else if self.is_http() {
127
            80
128
        } else {
129
            0
130
        }
131
1
    }
132
    
133
    /// Join a relative path to this URL
134
    #[cfg(feature = "http")]
135
    pub fn join(&self, path: &str) -> Result<Self, UrlParseError> {
136
        use ::url::Url as UrlParser;
137
        
138
        let base = UrlParser::parse(self.href.as_str())
139
            .map_err(|e| UrlParseError {
140
                message: AzString::from(e.to_string()),
141
            })?;
142
        
143
        let joined = base.join(path)
144
            .map_err(|e| UrlParseError {
145
                message: AzString::from(e.to_string()),
146
            })?;
147

            
148
        Self::parse(joined.as_str())
149
    }
150

            
151
    /// Stub: `http` feature disabled (the `url` crate is gated behind `http`).
152
    #[cfg(not(feature = "http"))]
153
    pub fn parse(_s: &str) -> Result<Self, UrlParseError> {
154
        Err(UrlParseError {
155
            message: AzString::from("http feature not enabled".to_string()),
156
        })
157
    }
158

            
159
    /// Stub: `http` feature disabled (the `url` crate is gated behind `http`).
160
    #[cfg(not(feature = "http"))]
161
    pub fn join(&self, _path: &str) -> Result<Self, UrlParseError> {
162
        Err(UrlParseError {
163
            message: AzString::from("http feature not enabled".to_string()),
164
        })
165
    }
166
}
167

            
168
impl fmt::Display for Url {
169
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170
        write!(f, "{}", self.href.as_str())
171
    }
172
}
173

            
174
#[cfg(test)]
175
mod tests {
176
    use super::*;
177
    
178
    #[test]
179
    #[cfg(feature = "http")]
180
    fn test_url_parse() {
181
        let url = Url::parse("https://example.com:8080/path?query=1#frag").unwrap();
182
        assert_eq!(url.scheme.as_str(), "https");
183
        assert_eq!(url.host.as_str(), "example.com");
184
        assert_eq!(url.port, 8080);
185
        assert_eq!(url.path.as_str(), "/path");
186
        assert_eq!(url.query.as_str(), "query=1");
187
        assert_eq!(url.fragment.as_str(), "frag");
188
    }
189
    
190
    #[test]
191
1
    fn test_url_from_parts() {
192
1
        let url = Url::from_parts("https", "example.com", 443, "/api");
193
1
        assert!(url.is_https());
194
1
        assert_eq!(url.effective_port(), 443);
195
1
    }
196
}