1mod canonicalize;
43mod compare;
44pub mod config;
45mod errors;
46mod proof;
47mod types;
48
49pub use canonicalize::{canonicalize_json, canonicalize_query, canonicalize_urlencoded};
50pub use compare::timing_safe_equal;
51pub use errors::{AshError, AshErrorCode};
52pub use proof::{
53 build_proof,
54 build_proof_v21,
55 build_proof_v21_scoped,
56 build_proof_v21_unified,
57 derive_client_secret,
58 extract_scoped_fields,
60 generate_context_id,
61 generate_nonce,
63 hash_body,
64 hash_proof,
65 hash_scoped_body,
66 verify_proof,
67 verify_proof_v21,
68 verify_proof_v21_scoped,
69 verify_proof_v21_unified,
70 UnifiedProofResult,
72};
73pub use types::{AshMode, BuildProofInput, VerifyInput};
74
75pub fn normalize_binding(method: &str, path: &str, query: &str) -> Result<String, AshError> {
99 let method = method.trim().to_uppercase();
101 if method.is_empty() {
102 return Err(AshError::new(
103 AshErrorCode::MalformedRequest,
104 "Method cannot be empty",
105 ));
106 }
107
108 let path = path.trim();
110 if !path.starts_with('/') {
111 return Err(AshError::new(
112 AshErrorCode::MalformedRequest,
113 "Path must start with /",
114 ));
115 }
116
117 let path_only = path.split('?').next().unwrap_or(path);
119
120 let mut normalized_path = String::with_capacity(path_only.len());
122 let mut prev_slash = false;
123
124 for ch in path_only.chars() {
125 if ch == '/' {
126 if !prev_slash {
127 normalized_path.push(ch);
128 }
129 prev_slash = true;
130 } else {
131 normalized_path.push(ch);
132 prev_slash = false;
133 }
134 }
135
136 if normalized_path.len() > 1 && normalized_path.ends_with('/') {
138 normalized_path.pop();
139 }
140
141 let canonical_query = if query.is_empty() {
143 String::new()
144 } else {
145 canonicalize::canonicalize_query(query)?
146 };
147
148 Ok(format!(
150 "{}|{}|{}",
151 method, normalized_path, canonical_query
152 ))
153}
154
155pub fn normalize_binding_from_url(method: &str, full_path: &str) -> Result<String, AshError> {
168 let (path, query) = match full_path.find('?') {
169 Some(pos) => (&full_path[..pos], &full_path[pos + 1..]),
170 None => (full_path, ""),
171 };
172 normalize_binding(method, path, query)
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
182 fn test_normalize_binding_basic() {
183 assert_eq!(
184 normalize_binding("POST", "/api/users", "").unwrap(),
185 "POST|/api/users|"
186 );
187 }
188
189 #[test]
190 fn test_normalize_binding_lowercase_method() {
191 assert_eq!(
192 normalize_binding("post", "/api/users", "").unwrap(),
193 "POST|/api/users|"
194 );
195 }
196
197 #[test]
198 fn test_normalize_binding_duplicate_slashes() {
199 assert_eq!(
200 normalize_binding("GET", "/api//users///profile", "").unwrap(),
201 "GET|/api/users/profile|"
202 );
203 }
204
205 #[test]
206 fn test_normalize_binding_trailing_slash() {
207 assert_eq!(
208 normalize_binding("PUT", "/api/users/", "").unwrap(),
209 "PUT|/api/users|"
210 );
211 }
212
213 #[test]
214 fn test_normalize_binding_root() {
215 assert_eq!(normalize_binding("GET", "/", "").unwrap(), "GET|/|");
216 }
217
218 #[test]
219 fn test_normalize_binding_with_query() {
220 assert_eq!(
221 normalize_binding("GET", "/api/users", "page=1&sort=name").unwrap(),
222 "GET|/api/users|page=1&sort=name"
223 );
224 }
225
226 #[test]
227 fn test_normalize_binding_query_sorted() {
228 assert_eq!(
229 normalize_binding("GET", "/api/users", "z=3&a=1&b=2").unwrap(),
230 "GET|/api/users|a=1&b=2&z=3"
231 );
232 }
233
234 #[test]
235 fn test_normalize_binding_from_url_basic() {
236 assert_eq!(
237 normalize_binding_from_url("GET", "/api/users?page=1&sort=name").unwrap(),
238 "GET|/api/users|page=1&sort=name"
239 );
240 }
241
242 #[test]
243 fn test_normalize_binding_from_url_no_query() {
244 assert_eq!(
245 normalize_binding_from_url("POST", "/api/users").unwrap(),
246 "POST|/api/users|"
247 );
248 }
249
250 #[test]
251 fn test_normalize_binding_from_url_query_sorted() {
252 assert_eq!(
253 normalize_binding_from_url("GET", "/api/search?z=last&a=first").unwrap(),
254 "GET|/api/search|a=first&z=last"
255 );
256 }
257
258 #[test]
259 fn test_normalize_binding_empty_method() {
260 assert!(normalize_binding("", "/api", "").is_err());
261 }
262
263 #[test]
264 fn test_normalize_binding_no_leading_slash() {
265 assert!(normalize_binding("GET", "api/users", "").is_err());
266 }
267}