1use crate::DpopError;
2use url::{Position, Url};
3
4pub fn normalize_htu(input: &str) -> Result<String, DpopError> {
11 let mut url = Url::parse(input).map_err(|_| DpopError::MalformedHtu)?;
12 let scheme = url.scheme().to_ascii_lowercase();
13 if scheme != "http" && scheme != "https" {
14 return Err(DpopError::MalformedHtu);
15 }
16
17 match url.host_str() {
18 Some(host) if !host.is_empty() => {
19 let lower = host.to_ascii_lowercase();
20 url.set_host(Some(&lower))
21 .map_err(|_| DpopError::MalformedHtu)?;
22 }
23 _ => {
24 return Err(DpopError::MalformedHtu);
25 }
26 }
27
28 url.set_fragment(None);
29 url.set_query(None);
30
31 let is_default = (scheme == "http" && url.port() == Some(80))
32 || (scheme == "https" && url.port() == Some(443));
33 if is_default {
34 let _ = url.set_port(None);
35 }
36 if url.path().is_empty() {
37 url.set_path("/");
38 }
39 let mut norm: Vec<&str> = Vec::new();
41 {
42 let segs = url.path_segments().ok_or(DpopError::MalformedHtu)?;
43 for s in segs {
44 match s {
45 "" | "." => {}
46 ".." => {
47 norm.pop();
48 }
49 other => norm.push(other),
50 }
51 }
52 }
53 let mut new_path = String::from("/");
54 new_path.push_str(&norm.join("/"));
55 url.set_path(&new_path);
56
57 Ok(url[..Position::AfterPath].to_string())
58}
59
60pub fn normalize_method(method: &str) -> Result<String, DpopError> {
62 let uppercase_method = method.trim().to_ascii_uppercase();
63 if !matches!(
65 uppercase_method.as_str(),
66 "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "TRACE"
67 ) {
68 return Err(DpopError::InvalidMethod);
69 }
70 Ok(uppercase_method)
71}
72
73#[cfg(test)]
74mod tests {
75 use super::normalize_htu;
76 use crate::DpopError;
77
78 #[test]
79 fn lowercases_scheme_and_host_and_strips_qf_and_default_ports() {
80 let got = normalize_htu("HTTPS://EXAMPLE.com:443/Api/v1?q=1#frag").unwrap();
81 assert_eq!(got, "https://example.com/Api/v1");
82 }
83
84 #[test]
85 fn keeps_non_default_port() {
86 let got = normalize_htu("http://example.com:8080/a/b").unwrap();
87 assert_eq!(got, "http://example.com:8080/a/b");
88 }
89
90 #[test]
91 fn empty_path_becomes_root() {
92 let got = normalize_htu("https://example.com").unwrap();
93 assert_eq!(got, "https://example.com/");
94 }
95
96 #[test]
97 fn resolves_dot_segments() {
98 let got = normalize_htu("https://ex.com/a/./b/../c").unwrap();
99 assert_eq!(got, "https://ex.com/a/c");
100 }
101
102 #[test]
103 fn collapses_redundant_slashes() {
104 let got = normalize_htu("https://ex.com//a///b////c").unwrap();
106 assert_eq!(got, "https://ex.com/a/b/c");
107 }
108
109 #[test]
110 fn ipv6_and_default_port_elision() {
111 let got = normalize_htu("https://[2001:db8::1]:443/a").unwrap();
112 assert_eq!(got, "https://[2001:db8::1]/a");
113 }
114
115 #[test]
116 fn ipv6_non_default_port_kept() {
117 let got = normalize_htu("http://[2001:db8::1]:8080/a").unwrap();
118 assert_eq!(got, "http://[2001:db8::1]:8080/a");
119 }
120
121 #[test]
122 fn rejects_non_http_schemes() {
123 let err = normalize_htu("ftp://example.com/a").unwrap_err();
124 assert!(matches!(err, DpopError::MalformedHtu));
125 }
126
127 #[test]
128 fn rejects_missing_or_empty_host() {
129 assert!(matches!(
130 normalize_htu("https:"),
131 Err(DpopError::MalformedHtu)
132 ));
133 assert!(matches!(
134 normalize_htu("https://"),
135 Err(DpopError::MalformedHtu)
136 ));
137 }
138
139 #[test]
140 fn idempotent_when_already_normalized() {
141 let input = "https://example.com/a/b";
142 let once = normalize_htu(input).unwrap();
143 let twice = normalize_htu(&once).unwrap();
144 assert_eq!(once, twice);
145 assert_eq!(twice, "https://example.com/a/b");
146 }
147
148 #[test]
149 fn preserves_path_case() {
150 let got = normalize_htu("https://EX.com/Api/V1/Users").unwrap();
152 assert_eq!(got, "https://ex.com/Api/V1/Users");
153 }
154
155 #[test]
156 fn removes_query_and_fragment_only() {
157 let got = normalize_htu("http://ex.com/a/b?x=1&y=2#sec").unwrap();
158 assert_eq!(got, "http://ex.com/a/b");
159 }
160
161 #[test]
162 fn resolves_trailing_parent_segment() {
163 let got = normalize_htu("http://ex.com/a/b/..").unwrap();
164 assert_eq!(got, "http://ex.com/a");
165 }
166}