crates_docs/tools/docs/cache/
key.rs1use std::collections::hash_map::DefaultHasher;
4use std::hash::{Hash, Hasher};
5
6#[inline]
8fn is_valid_crate_name_char(b: u8) -> bool {
9 b.is_ascii_alphanumeric() || b == b'_' || b == b'-'
10}
11
12#[inline]
14fn is_valid_item_path_char(b: u8) -> bool {
15 b.is_ascii_alphanumeric() || b == b'_' || b == b'-' || b == b':'
16}
17
18#[inline]
20fn is_valid_crate_name(name: &str) -> bool {
21 !name.is_empty() && name.bytes().all(is_valid_crate_name_char)
22}
23
24#[inline]
33fn is_valid_item_path(path: &str) -> bool {
34 !path.is_empty()
35 && path.bytes().all(is_valid_item_path_char)
36 && !path.replace("::", "").contains(':')
37}
38
39#[inline]
48fn escape_key_segment(segment: &str) -> String {
49 if segment.contains('%') || segment.contains(':') {
50 segment.replace('%', "%25").replace(':', "%3a")
51 } else {
52 segment.to_string()
53 }
54}
55
56#[inline]
68fn normalize_cache_version(version: Option<&str>) -> Option<String> {
69 version
70 .map(|v| v.trim().to_lowercase())
71 .filter(|v| !v.is_empty() && v != "latest")
72}
73
74pub struct CacheKeyGenerator;
76
77impl CacheKeyGenerator {
78 #[must_use]
92 pub fn crate_html_cache_key(crate_name: &str, version: Option<&str>) -> String {
93 let base_key = Self::crate_cache_key(crate_name, version);
94 format!("htmlraw:{base_key}")
95 }
96
97 #[must_use]
107 pub fn crate_cache_key(crate_name: &str, version: Option<&str>) -> String {
108 let normalized_name = crate_name.trim().to_lowercase();
110 let normalized_ver = normalize_cache_version(version);
111
112 if !is_valid_crate_name(&normalized_name) {
113 let mut hasher = DefaultHasher::new();
114 normalized_name.hash(&mut hasher);
115 let hash = hasher.finish();
116 return match normalized_ver {
117 Some(ver) => format!("crate:hash:{hash}:{ver}"),
118 None => format!("crate:hash:{hash}"),
119 };
120 }
121
122 match normalized_ver {
123 Some(ver) => format!("crate:{normalized_name}:{ver}"),
124 None => format!("crate:{normalized_name}"),
125 }
126 }
127
128 #[must_use]
135 pub fn search_cache_key(query: &str, limit: u32, sort: Option<&str>) -> String {
136 let normalized_query = escape_key_segment(&query.trim().to_lowercase());
137 let normalized_sort =
138 escape_key_segment(&sort.unwrap_or("relevance").trim().to_lowercase());
139 format!("search:{normalized_query}:{normalized_sort}:{limit}")
140 }
141
142 #[must_use]
151 pub fn item_cache_key(crate_name: &str, item_path: &str, version: Option<&str>) -> String {
152 let normalized_name = crate_name.trim().to_lowercase();
153 let normalized_path = item_path.trim();
154 let normalized_ver = normalize_cache_version(version);
155
156 if !is_valid_crate_name(&normalized_name) || !is_valid_item_path(normalized_path) {
157 let mut hasher = DefaultHasher::new();
158 normalized_name.hash(&mut hasher);
159 normalized_path.hash(&mut hasher);
160 let hash = hasher.finish();
161 return match normalized_ver {
162 Some(ver) => {
163 format!("item:{normalized_name}:{ver}:hash:{hash}")
164 }
165 None => format!("item:{normalized_name}:hash:{hash}"),
166 };
167 }
168
169 match normalized_ver {
170 Some(ver) => {
171 format!("item:{normalized_name}:{ver}:{normalized_path}")
172 }
173 None => format!("item:{normalized_name}:{normalized_path}"),
174 }
175 }
176
177 #[must_use]
185 pub fn item_html_cache_key(crate_name: &str, item_path: &str, version: Option<&str>) -> String {
186 let base_key = Self::item_cache_key(crate_name, item_path, version);
187 format!("htmlraw:{base_key}")
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_cache_key_generation() {
197 assert_eq!(
198 CacheKeyGenerator::crate_cache_key("serde", None),
199 "crate:serde"
200 );
201 assert_eq!(
202 CacheKeyGenerator::crate_cache_key("serde", Some("1.0")),
203 "crate:serde:1.0"
204 );
205 assert_eq!(
206 CacheKeyGenerator::crate_html_cache_key("serde", Some("1.0")),
207 "htmlraw:crate:serde:1.0"
208 );
209
210 assert_eq!(
211 CacheKeyGenerator::search_cache_key("web framework", 10, None),
212 "search:web framework:relevance:10"
213 );
214 assert_eq!(
215 CacheKeyGenerator::search_cache_key("web framework", 10, Some("downloads")),
216 "search:web framework:downloads:10"
217 );
218
219 assert_eq!(
220 CacheKeyGenerator::item_cache_key("serde", "Serialize", None),
221 "item:serde:Serialize"
222 );
223 assert_eq!(
224 CacheKeyGenerator::item_cache_key("serde", "Serialize", Some("1.0")),
225 "item:serde:1.0:Serialize"
226 );
227 assert_eq!(
228 CacheKeyGenerator::item_html_cache_key("serde", "Serialize", Some("1.0")),
229 "htmlraw:item:serde:1.0:Serialize"
230 );
231 }
232
233 #[test]
234 fn test_cache_key_normalization_case_insensitivity() {
235 assert_eq!(
236 CacheKeyGenerator::crate_cache_key("Serde", None),
237 CacheKeyGenerator::crate_cache_key("serde", None)
238 );
239 assert_eq!(
240 CacheKeyGenerator::crate_cache_key("SERDE", None),
241 CacheKeyGenerator::crate_cache_key("serde", None)
242 );
243
244 assert_eq!(
245 CacheKeyGenerator::crate_cache_key("Tokio", Some("1.0")),
246 CacheKeyGenerator::crate_cache_key("tokio", Some("1.0"))
247 );
248
249 assert_eq!(
250 CacheKeyGenerator::search_cache_key("Web Framework", 10, Some("Relevance")),
251 CacheKeyGenerator::search_cache_key("web framework", 10, Some("relevance"))
252 );
253
254 assert_eq!(
255 CacheKeyGenerator::item_cache_key("Serde", "Serialize", None),
256 CacheKeyGenerator::item_cache_key("serde", "Serialize", None)
257 );
258 }
259
260 #[test]
261 fn test_cache_key_normalization_whitespace() {
262 assert_eq!(
263 CacheKeyGenerator::crate_cache_key("serde", Some(" 1.0 ")),
264 "crate:serde:1.0"
265 );
266
267 assert_eq!(
268 CacheKeyGenerator::search_cache_key(" web framework ", 10, Some(" downloads ")),
269 "search:web framework:downloads:10"
270 );
271
272 assert_eq!(
273 CacheKeyGenerator::item_cache_key("serde", " Serialize ", None),
274 "item:serde:Serialize"
275 );
276 }
277
278 #[test]
279 fn test_html_artifact_keys_do_not_collide_with_rendered_keys() {
280 let rendered = CacheKeyGenerator::crate_cache_key("serde", Some("html"));
283 let artifact = CacheKeyGenerator::crate_html_cache_key("serde", None);
284 assert_ne!(rendered, artifact);
285 assert_eq!(rendered, "crate:serde:html");
286 assert_eq!(artifact, "htmlraw:crate:serde");
287
288 let rendered_item = CacheKeyGenerator::item_cache_key("serde", "Serialize", Some("html"));
289 let artifact_item = CacheKeyGenerator::item_html_cache_key("serde", "Serialize", None);
290 assert_ne!(rendered_item, artifact_item);
291 }
292
293 #[test]
294 fn test_cache_key_normalization_version_case() {
295 assert_eq!(
296 CacheKeyGenerator::crate_cache_key("serde", Some("1.0-RC1")),
297 "crate:serde:1.0-rc1"
298 );
299 assert_eq!(
300 CacheKeyGenerator::item_cache_key("serde", "Serialize", Some("V1.0")),
301 "item:serde:v1.0:Serialize"
302 );
303 }
304
305 #[test]
306 fn test_cache_key_injection_prevention() {
307 let malicious_key = CacheKeyGenerator::crate_cache_key("serde:malicious", None);
308 assert!(malicious_key.starts_with("crate:hash:"));
309 assert!(!malicious_key.contains("serde:malicious"));
310
311 let malicious_key_with_version =
312 CacheKeyGenerator::crate_cache_key("crate:evil", Some("1.0"));
313 assert!(malicious_key_with_version.starts_with("crate:hash:"));
314 assert!(!malicious_key_with_version.contains("crate:evil"));
315
316 let valid_key = CacheKeyGenerator::crate_cache_key("serde-json", None);
317 assert_eq!(valid_key, "crate:serde-json");
318
319 let valid_key_underscore = CacheKeyGenerator::crate_cache_key("my_crate", None);
320 assert_eq!(valid_key_underscore, "crate:my_crate");
321 }
322
323 #[test]
324 fn test_search_cache_key_no_colon_collision() {
325 let a = CacheKeyGenerator::search_cache_key("a", 5, Some("b:c"));
328 let b = CacheKeyGenerator::search_cache_key("a:b", 5, Some("c"));
329 assert_ne!(a, b);
330
331 assert_eq!(
333 CacheKeyGenerator::search_cache_key("web framework", 10, Some("downloads")),
334 "search:web framework:downloads:10"
335 );
336
337 let pct = CacheKeyGenerator::search_cache_key("100%", 10, None);
339 let escaped = CacheKeyGenerator::search_cache_key("100%3a", 10, None);
340 assert_ne!(pct, escaped);
341 }
342
343 #[test]
344 fn test_item_cache_key_no_version_path_collision() {
345 let a = CacheKeyGenerator::item_cache_key("serde", "1.0:Serialize", None);
348 let b = CacheKeyGenerator::item_cache_key("serde", "Serialize", Some("1.0"));
349 assert_ne!(a, b);
350 assert_eq!(
352 CacheKeyGenerator::item_cache_key("serde", "de::Deserialize", None),
353 "item:serde:de::Deserialize"
354 );
355 }
356
357 #[test]
358 fn test_cache_key_version_latest_normalizes_to_none() {
359 let none = CacheKeyGenerator::crate_cache_key("serde", None);
362 assert_eq!(none, "crate:serde");
363 assert_eq!(
364 CacheKeyGenerator::crate_cache_key("serde", Some("latest")),
365 none
366 );
367 assert_eq!(
368 CacheKeyGenerator::crate_cache_key("serde", Some("LATEST")),
369 none
370 );
371 assert_eq!(
372 CacheKeyGenerator::crate_cache_key("serde", Some(" latest ")),
373 none
374 );
375 assert_eq!(CacheKeyGenerator::crate_cache_key("serde", Some("")), none);
376 assert_eq!(
377 CacheKeyGenerator::crate_cache_key("serde", Some(" ")),
378 none
379 );
380
381 assert_eq!(
383 CacheKeyGenerator::crate_html_cache_key("serde", Some("latest")),
384 CacheKeyGenerator::crate_html_cache_key("serde", None)
385 );
386
387 let item_none = CacheKeyGenerator::item_cache_key("serde", "Serialize", None);
389 assert_eq!(item_none, "item:serde:Serialize");
390 assert_eq!(
391 CacheKeyGenerator::item_cache_key("serde", "Serialize", Some("latest")),
392 item_none
393 );
394 assert_eq!(
395 CacheKeyGenerator::item_cache_key("serde", "Serialize", Some("")),
396 item_none
397 );
398
399 assert_eq!(
401 CacheKeyGenerator::crate_cache_key("serde", Some("1.0")),
402 "crate:serde:1.0"
403 );
404 }
405
406 #[test]
407 fn test_item_path_case_sensitivity() {
408 assert_ne!(
409 CacheKeyGenerator::item_cache_key("serde", "Serialize", None),
410 CacheKeyGenerator::item_cache_key("serde", "serialize", None)
411 );
412 }
413
414 #[test]
415 fn test_cache_key_edge_cases() {
416 let empty_key = CacheKeyGenerator::crate_cache_key("", None);
417 assert!(empty_key.starts_with("crate:hash:"));
418
419 let whitespace_key = CacheKeyGenerator::crate_cache_key(" ", None);
420 assert!(whitespace_key.starts_with("crate:hash:"));
421
422 assert_eq!(
425 CacheKeyGenerator::crate_cache_key("serde", Some("")),
426 "crate:serde"
427 );
428
429 let unicode_key = CacheKeyGenerator::crate_cache_key("serde测试", None);
430 assert!(unicode_key.starts_with("crate:hash:"));
431 assert!(!unicode_key.contains("测试"));
432
433 let malicious_item_path =
434 CacheKeyGenerator::item_cache_key("serde", "Serialize\nmalicious", None);
435 assert!(malicious_item_path.contains("hash:"));
436 assert!(!malicious_item_path.contains('\n'));
437
438 let malicious_item_colon =
441 CacheKeyGenerator::item_cache_key("serde", "Serialize:extra:colons", None);
442 assert!(malicious_item_colon.starts_with("item:serde:hash:"));
443 assert!(!malicious_item_colon.contains("Serialize:extra:colons"));
444
445 let valid_item_path = CacheKeyGenerator::item_cache_key("serde", "serde::Serialize", None);
446 assert_eq!(valid_item_path, "item:serde:serde::Serialize");
447
448 let empty_item_key = CacheKeyGenerator::item_cache_key("serde", "", None);
449 assert!(empty_item_key.contains("hash:"));
450
451 let empty_item_crate = CacheKeyGenerator::item_cache_key("", "Crate", None);
452 assert!(empty_item_crate.contains("hash:"));
453 }
454}