crates_docs/tools/docs/cache/
key.rs1use std::collections::hash_map::DefaultHasher;
4use std::hash::{Hash, Hasher};
5
6fn normalize_version(version: Option<&str>) -> Option<String> {
7 version.map(|v| v.trim().to_lowercase())
8}
9
10fn is_valid_crate_name_char(b: u8) -> bool {
11 b.is_ascii_alphanumeric() || b == b'_' || b == b'-'
12}
13
14fn is_valid_item_path_char(b: u8) -> bool {
15 b.is_ascii_alphanumeric() || b == b'_' || b == b'-' || b == b':'
16}
17
18fn is_valid_crate_name(name: &str) -> bool {
19 !name.is_empty() && name.bytes().all(is_valid_crate_name_char)
20}
21
22fn is_valid_item_path(path: &str) -> bool {
23 !path.is_empty() && path.bytes().all(is_valid_item_path_char)
24}
25
26pub struct CacheKeyGenerator;
28
29impl CacheKeyGenerator {
30 #[must_use]
39 pub fn crate_cache_key(crate_name: &str, version: Option<&str>) -> String {
40 let normalized_name = crate_name.to_lowercase();
41
42 if !is_valid_crate_name(&normalized_name) {
43 let mut hasher = DefaultHasher::new();
44 normalized_name.hash(&mut hasher);
45 let hash = hasher.finish();
46 return match normalize_version(version) {
47 Some(normalized_ver) => format!("crate:hash:{hash}:{normalized_ver}"),
48 None => format!("crate:hash:{hash}"),
49 };
50 }
51
52 match normalize_version(version) {
53 Some(normalized_ver) => format!("crate:{normalized_name}:{normalized_ver}"),
54 None => format!("crate:{normalized_name}"),
55 }
56 }
57
58 #[must_use]
64 pub fn search_cache_key(query: &str, limit: u32) -> String {
65 let normalized_query = query.trim().to_lowercase();
66 format!("search:{normalized_query}:{limit}")
67 }
68
69 #[must_use]
77 pub fn item_cache_key(crate_name: &str, item_path: &str, version: Option<&str>) -> String {
78 let normalized_name = crate_name.to_lowercase();
79 let normalized_path = item_path.trim();
80
81 if !is_valid_crate_name(&normalized_name) || !is_valid_item_path(normalized_path) {
82 let mut hasher = DefaultHasher::new();
83 normalized_name.hash(&mut hasher);
84 normalized_path.hash(&mut hasher);
85 let hash = hasher.finish();
86 return match normalize_version(version) {
87 Some(normalized_ver) => {
88 format!("item:{normalized_name}:{normalized_ver}:hash:{hash}")
89 }
90 None => format!("item:{normalized_name}:hash:{hash}"),
91 };
92 }
93
94 match normalize_version(version) {
95 Some(normalized_ver) => {
96 format!("item:{normalized_name}:{normalized_ver}:{normalized_path}")
97 }
98 None => format!("item:{normalized_name}:{normalized_path}"),
99 }
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn test_cache_key_generation() {
109 assert_eq!(
110 CacheKeyGenerator::crate_cache_key("serde", None),
111 "crate:serde"
112 );
113 assert_eq!(
114 CacheKeyGenerator::crate_cache_key("serde", Some("1.0")),
115 "crate:serde:1.0"
116 );
117
118 assert_eq!(
119 CacheKeyGenerator::search_cache_key("web framework", 10),
120 "search:web framework:10"
121 );
122
123 assert_eq!(
124 CacheKeyGenerator::item_cache_key("serde", "Serialize", None),
125 "item:serde:Serialize"
126 );
127 assert_eq!(
128 CacheKeyGenerator::item_cache_key("serde", "Serialize", Some("1.0")),
129 "item:serde:1.0:Serialize"
130 );
131 }
132
133 #[test]
134 fn test_cache_key_normalization_case_insensitivity() {
135 assert_eq!(
136 CacheKeyGenerator::crate_cache_key("Serde", None),
137 CacheKeyGenerator::crate_cache_key("serde", None)
138 );
139 assert_eq!(
140 CacheKeyGenerator::crate_cache_key("SERDE", None),
141 CacheKeyGenerator::crate_cache_key("serde", None)
142 );
143
144 assert_eq!(
145 CacheKeyGenerator::crate_cache_key("Tokio", Some("1.0")),
146 CacheKeyGenerator::crate_cache_key("tokio", Some("1.0"))
147 );
148
149 assert_eq!(
150 CacheKeyGenerator::search_cache_key("Web Framework", 10),
151 CacheKeyGenerator::search_cache_key("web framework", 10)
152 );
153
154 assert_eq!(
155 CacheKeyGenerator::item_cache_key("Serde", "Serialize", None),
156 CacheKeyGenerator::item_cache_key("serde", "Serialize", None)
157 );
158 }
159
160 #[test]
161 fn test_cache_key_normalization_whitespace() {
162 assert_eq!(
163 CacheKeyGenerator::crate_cache_key("serde", Some(" 1.0 ")),
164 "crate:serde:1.0"
165 );
166
167 assert_eq!(
168 CacheKeyGenerator::search_cache_key(" web framework ", 10),
169 "search:web framework:10"
170 );
171
172 assert_eq!(
173 CacheKeyGenerator::item_cache_key("serde", " Serialize ", None),
174 "item:serde:Serialize"
175 );
176 }
177
178 #[test]
179 fn test_cache_key_normalization_version_case() {
180 assert_eq!(
181 CacheKeyGenerator::crate_cache_key("serde", Some("1.0-RC1")),
182 "crate:serde:1.0-rc1"
183 );
184 assert_eq!(
185 CacheKeyGenerator::item_cache_key("serde", "Serialize", Some("V1.0")),
186 "item:serde:v1.0:Serialize"
187 );
188 }
189
190 #[test]
191 fn test_cache_key_injection_prevention() {
192 let malicious_key = CacheKeyGenerator::crate_cache_key("serde:malicious", None);
193 assert!(malicious_key.starts_with("crate:hash:"));
194 assert!(!malicious_key.contains("serde:malicious"));
195
196 let malicious_key_with_version =
197 CacheKeyGenerator::crate_cache_key("crate:evil", Some("1.0"));
198 assert!(malicious_key_with_version.starts_with("crate:hash:"));
199 assert!(!malicious_key_with_version.contains("crate:evil"));
200
201 let valid_key = CacheKeyGenerator::crate_cache_key("serde-json", None);
202 assert_eq!(valid_key, "crate:serde-json");
203
204 let valid_key_underscore = CacheKeyGenerator::crate_cache_key("my_crate", None);
205 assert_eq!(valid_key_underscore, "crate:my_crate");
206 }
207
208 #[test]
209 fn test_item_path_case_sensitivity() {
210 assert_ne!(
211 CacheKeyGenerator::item_cache_key("serde", "Serialize", None),
212 CacheKeyGenerator::item_cache_key("serde", "serialize", None)
213 );
214 }
215
216 #[test]
217 fn test_cache_key_edge_cases() {
218 let empty_key = CacheKeyGenerator::crate_cache_key("", None);
219 assert!(empty_key.starts_with("crate:hash:"));
220
221 let whitespace_key = CacheKeyGenerator::crate_cache_key(" ", None);
222 assert!(whitespace_key.starts_with("crate:hash:"));
223
224 assert_eq!(
225 CacheKeyGenerator::crate_cache_key("serde", Some("")),
226 "crate:serde:"
227 );
228
229 let unicode_key = CacheKeyGenerator::crate_cache_key("serde测试", None);
230 assert!(unicode_key.starts_with("crate:hash:"));
231 assert!(!unicode_key.contains("测试"));
232
233 let malicious_item_path =
234 CacheKeyGenerator::item_cache_key("serde", "Serialize\nmalicious", None);
235 assert!(malicious_item_path.contains("hash:"));
236 assert!(!malicious_item_path.contains('\n'));
237
238 let malicious_item_colon =
239 CacheKeyGenerator::item_cache_key("serde", "Serialize:extra:colons", None);
240 assert_eq!(malicious_item_colon, "item:serde:Serialize:extra:colons");
241
242 let valid_item_path = CacheKeyGenerator::item_cache_key("serde", "serde::Serialize", None);
243 assert_eq!(valid_item_path, "item:serde:serde::Serialize");
244
245 let empty_item_key = CacheKeyGenerator::item_cache_key("serde", "", None);
246 assert!(empty_item_key.contains("hash:"));
247
248 let empty_item_crate = CacheKeyGenerator::item_cache_key("", "Crate", None);
249 assert!(empty_item_crate.contains("hash:"));
250 }
251}