1mod canonicalize;
43mod compare;
44mod errors;
45mod proof;
46mod types;
47
48pub use canonicalize::{canonicalize_json, canonicalize_urlencoded};
49pub use compare::timing_safe_equal;
50pub use errors::{AshError, AshErrorCode};
51pub use proof::{
52 build_proof, verify_proof,
53 generate_nonce, generate_context_id,
55 derive_client_secret, build_proof_v21,
56 verify_proof_v21, hash_body,
57 extract_scoped_fields, build_proof_v21_scoped,
59 verify_proof_v21_scoped, hash_scoped_body,
60 UnifiedProofResult, hash_proof,
62 build_proof_v21_unified, verify_proof_v21_unified,
63};
64pub use types::{AshMode, BuildProofInput, VerifyInput};
65
66pub fn normalize_binding(method: &str, path: &str) -> Result<String, AshError> {
86 let method = method.trim().to_uppercase();
88 if method.is_empty() {
89 return Err(AshError::new(
90 AshErrorCode::MalformedRequest,
91 "Method cannot be empty",
92 ));
93 }
94
95 let path = path.trim();
97 if !path.starts_with('/') {
98 return Err(AshError::new(
99 AshErrorCode::MalformedRequest,
100 "Path must start with /",
101 ));
102 }
103
104 let path = path.split('?').next().unwrap_or(path);
106
107 let mut normalized = String::with_capacity(path.len());
109 let mut prev_slash = false;
110
111 for ch in path.chars() {
112 if ch == '/' {
113 if !prev_slash {
114 normalized.push(ch);
115 }
116 prev_slash = true;
117 } else {
118 normalized.push(ch);
119 prev_slash = false;
120 }
121 }
122
123 if normalized.len() > 1 && normalized.ends_with('/') {
125 normalized.pop();
126 }
127
128 Ok(format!("{} {}", method, normalized))
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_normalize_binding_basic() {
137 assert_eq!(
138 normalize_binding("POST", "/api/users").unwrap(),
139 "POST /api/users"
140 );
141 }
142
143 #[test]
144 fn test_normalize_binding_lowercase_method() {
145 assert_eq!(
146 normalize_binding("post", "/api/users").unwrap(),
147 "POST /api/users"
148 );
149 }
150
151 #[test]
152 fn test_normalize_binding_duplicate_slashes() {
153 assert_eq!(
154 normalize_binding("GET", "/api//users///profile").unwrap(),
155 "GET /api/users/profile"
156 );
157 }
158
159 #[test]
160 fn test_normalize_binding_trailing_slash() {
161 assert_eq!(
162 normalize_binding("PUT", "/api/users/").unwrap(),
163 "PUT /api/users"
164 );
165 }
166
167 #[test]
168 fn test_normalize_binding_root() {
169 assert_eq!(normalize_binding("GET", "/").unwrap(), "GET /");
170 }
171
172 #[test]
173 fn test_normalize_binding_query_string() {
174 assert_eq!(
175 normalize_binding("GET", "/api/users?page=1").unwrap(),
176 "GET /api/users"
177 );
178 }
179
180 #[test]
181 fn test_normalize_binding_empty_method() {
182 assert!(normalize_binding("", "/api").is_err());
183 }
184
185 #[test]
186 fn test_normalize_binding_no_leading_slash() {
187 assert!(normalize_binding("GET", "api/users").is_err());
188 }
189}