rez_next_version/
parser.rs1use super::Version;
4use ahash::AHashMap;
5use once_cell::sync::Lazy;
6use rez_next_common::RezCoreError;
7use smallvec::SmallVec;
8use std::sync::RwLock;
9
10static STRING_INTERN_POOL: Lazy<RwLock<AHashMap<String, &'static str>>> =
12 Lazy::new(|| RwLock::new(AHashMap::new()));
13
14#[derive(Debug, Clone, PartialEq)]
16pub enum TokenType {
17 Numeric(u64),
18 Alphanumeric(String),
19 Separator(char),
20}
21
22#[derive(Debug, Clone, Copy, PartialEq)]
24enum ParseState {
25 Start,
26 InToken,
27 InSeparator,
28}
29
30pub struct StateMachineParser {
32 use_interning: bool,
34 max_tokens: usize,
36 max_numeric_tokens: usize,
38}
39
40impl StateMachineParser {
41 #[must_use]
43 pub fn new() -> Self {
44 Self {
45 use_interning: true,
46 max_tokens: 10,
47 max_numeric_tokens: 5,
48 }
49 }
50
51 #[must_use]
53 pub fn with_config(use_interning: bool, max_tokens: usize, max_numeric_tokens: usize) -> Self {
54 Self {
55 use_interning,
56 max_tokens,
57 max_numeric_tokens,
58 }
59 }
60
61 fn intern_string(&self, s: String) -> String {
63 if !self.use_interning || s.len() > 64 {
64 return s;
65 }
66
67 {
69 let pool = STRING_INTERN_POOL.read().unwrap();
70 if let Some(&interned) = pool.get(&s) {
71 return interned.to_string();
72 }
73 }
74
75 {
77 let mut pool = STRING_INTERN_POOL.write().unwrap();
78 if let Some(&interned) = pool.get(&s) {
80 return interned.to_string();
81 }
82
83 if pool.len() < 10000 {
85 let leaked: &'static str = Box::leak(s.clone().into_boxed_str());
86 pool.insert(s.clone(), leaked);
87 return leaked.to_string();
88 }
89 }
90
91 s
92 }
93
94 fn is_valid_separator(c: char) -> bool {
96 matches!(c, '.' | '-' | '_' | '+')
97 }
98
99 fn is_token_char(c: char) -> bool {
101 c.is_ascii_alphanumeric() || c == '_'
102 }
103
104 #[allow(clippy::type_complexity)]
106 #[allow(clippy::missing_errors_doc)]
107 pub fn parse_tokens(
108 &self,
109 input: &str,
110 ) -> Result<(SmallVec<[TokenType; 8]>, SmallVec<[char; 7]>), RezCoreError> {
111 if input.is_empty() {
112 return Ok((SmallVec::new(), SmallVec::new()));
113 }
114
115 let mut tokens = SmallVec::new();
116 let mut separators = SmallVec::new();
117 let mut state = ParseState::Start;
118 let mut current_token = String::new();
119 let mut numeric_count = 0;
120
121 let chars: SmallVec<[char; 64]> = input.chars().collect();
122 let mut i = 0;
123
124 while i < chars.len() {
125 let c = chars[i];
126
127 match state {
128 ParseState::Start => {
129 if Self::is_token_char(c) {
130 current_token.push(c);
131 state = ParseState::InToken;
132 } else if Self::is_valid_separator(c) {
133 return Err(RezCoreError::VersionParse(format!(
134 "Version cannot start with separator '{c}'"
135 )));
136 } else {
137 return Err(RezCoreError::VersionParse(format!(
138 "Invalid character '{c}' at start of version"
139 )));
140 }
141 }
142
143 ParseState::InToken => {
144 if Self::is_token_char(c) {
145 current_token.push(c);
146 } else if Self::is_valid_separator(c) {
147 self.finalize_token(&mut current_token, &mut tokens, &mut numeric_count)?;
149 separators.push(c);
150 state = ParseState::InSeparator;
151 } else {
152 return Err(RezCoreError::VersionParse(format!(
153 "Invalid character '{c}' in token"
154 )));
155 }
156 }
157
158 ParseState::InSeparator => {
159 if Self::is_token_char(c) {
160 current_token.push(c);
161 state = ParseState::InToken;
162 } else {
163 return Err(RezCoreError::VersionParse(format!(
164 "Expected token character after separator, found '{c}'"
165 )));
166 }
167 }
168 }
169
170 i += 1;
171 }
172
173 if state == ParseState::InToken && !current_token.is_empty() {
175 self.finalize_token(&mut current_token, &mut tokens, &mut numeric_count)?;
176 } else if state == ParseState::InSeparator {
177 return Err(RezCoreError::VersionParse(
178 "Version cannot end with separator".to_string(),
179 ));
180 }
181
182 if tokens.len() > self.max_tokens {
184 return Err(RezCoreError::VersionParse(format!(
185 "Too many tokens: {} (max: {})",
186 tokens.len(),
187 self.max_tokens
188 )));
189 }
190
191 if numeric_count > self.max_numeric_tokens {
192 return Err(RezCoreError::VersionParse(format!(
193 "Too many numeric tokens: {} (max: {})",
194 numeric_count, self.max_numeric_tokens
195 )));
196 }
197
198 Ok((tokens, separators))
199 }
200
201 #[allow(clippy::missing_errors_doc)]
203 fn finalize_token(
204 &self,
205 current_token: &mut String,
206 tokens: &mut SmallVec<[TokenType; 8]>,
207 numeric_count: &mut usize,
208 ) -> Result<(), RezCoreError> {
209 if current_token.is_empty() {
210 return Err(RezCoreError::VersionParse("Empty token found".to_string()));
211 }
212
213 if current_token.starts_with('_') || current_token.ends_with('_') {
215 return Err(RezCoreError::VersionParse(format!(
216 "Invalid token format: '{current_token}'"
217 )));
218 }
219
220 if current_token == "not" || current_token == "version" {
222 return Err(RezCoreError::VersionParse(format!(
223 "Invalid version token: '{current_token}'"
224 )));
225 }
226
227 if current_token.chars().all(char::is_alphabetic) && current_token.len() > 10 {
229 return Err(RezCoreError::VersionParse(format!(
230 "Invalid version token: '{current_token}'"
231 )));
232 }
233
234 if current_token.chars().all(|c| c.is_ascii_digit()) {
236 if let Ok(num) = current_token.parse::<u64>() {
237 tokens.push(TokenType::Numeric(num));
238 *numeric_count += 1;
239 } else {
240 let interned = self.intern_string(current_token.clone());
242 tokens.push(TokenType::Alphanumeric(interned));
243 }
244 } else {
245 let interned = self.intern_string(current_token.clone());
247 tokens.push(TokenType::Alphanumeric(interned));
248 }
249
250 current_token.clear();
251 Ok(())
252 }
253}
254
255pub struct VersionParser {
257 _inner: StateMachineParser,
258}
259
260impl VersionParser {
261 #[must_use]
263 pub fn new() -> Self {
264 Self {
265 _inner: StateMachineParser::new(),
266 }
267 }
268
269 #[allow(clippy::missing_errors_doc)]
271 pub fn parse_version(&self, input: &str) -> Result<Version, RezCoreError> {
272 Version::parse(input)
275 }
276}
277
278impl Default for VersionParser {
279 fn default() -> Self {
280 Self::new()
281 }
282}
283
284impl Default for StateMachineParser {
285 fn default() -> Self {
286 Self::new()
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_parser_creation() {
296 let _parser = VersionParser::new();
297 let _state_machine_parser = StateMachineParser::new();
298 }
300
301 #[test]
302 fn test_state_machine_parser_basic() {
303 let parser = StateMachineParser::new();
304
305 let (tokens, separators) = parser.parse_tokens("").unwrap();
307 assert!(tokens.is_empty());
308 assert!(separators.is_empty());
309
310 let (tokens, separators) = parser.parse_tokens("1.2.3").unwrap();
312 assert_eq!(tokens.len(), 3);
313 assert_eq!(separators.len(), 2);
314
315 match &tokens[0] {
317 TokenType::Numeric(n) => assert_eq!(*n, 1),
318 _ => panic!("Expected numeric token"),
319 }
320
321 assert_eq!(separators[0], '.');
322 assert_eq!(separators[1], '.');
323 }
324
325 #[test]
326 fn test_state_machine_parser_alphanumeric() {
327 let parser = StateMachineParser::new();
328
329 let (tokens, separators) = parser.parse_tokens("1.2.3-alpha1").unwrap();
330 assert_eq!(tokens.len(), 4);
331 assert_eq!(separators.len(), 3);
332
333 match &tokens[0] {
335 TokenType::Numeric(n) => assert_eq!(*n, 1),
336 _ => panic!("Expected numeric token"),
337 }
338
339 match &tokens[3] {
340 TokenType::Alphanumeric(s) => assert_eq!(s, "alpha1"),
341 _ => panic!("Expected alphanumeric token"),
342 }
343 }
344
345 #[test]
346 fn test_state_machine_parser_errors() {
347 let parser = StateMachineParser::new();
348
349 assert!(parser.parse_tokens(".1.2.3").is_err());
351
352 assert!(parser.parse_tokens("1.2.3.").is_err());
354
355 assert!(parser.parse_tokens("1.2.3@").is_err());
357
358 assert!(parser.parse_tokens("_invalid").is_err());
360 assert!(parser.parse_tokens("invalid_").is_err());
361 }
362
363 #[test]
364 fn test_string_interning() {
365 let parser = StateMachineParser::with_config(true, 10, 5);
366
367 let (tokens1, _) = parser.parse_tokens("1.0.0-alpha").unwrap();
369 let (tokens2, _) = parser.parse_tokens("1.0.0-alpha").unwrap();
370
371 if let (TokenType::Alphanumeric(s1), TokenType::Alphanumeric(s2)) =
373 (&tokens1[3], &tokens2[3])
374 {
375 assert_eq!(s1, s2);
377 }
378 }
379
380 #[test]
381 fn test_performance_limits() {
382 let parser = StateMachineParser::new();
383
384 let too_many_tokens = (0..15).map(|i| i.to_string()).collect::<Vec<_>>().join(".");
386 assert!(parser.parse_tokens(&too_many_tokens).is_err());
387
388 let too_many_numeric = (0..10).map(|i| i.to_string()).collect::<Vec<_>>().join(".");
390 assert!(parser.parse_tokens(&too_many_numeric).is_err());
391 }
392
393 #[test]
394 fn test_character_classification() {
395 assert!(StateMachineParser::is_valid_separator('.'));
396 assert!(StateMachineParser::is_valid_separator('-'));
397 assert!(StateMachineParser::is_valid_separator('_'));
398 assert!(StateMachineParser::is_valid_separator('+'));
399 assert!(!StateMachineParser::is_valid_separator('@'));
400
401 assert!(StateMachineParser::is_token_char('a'));
402 assert!(StateMachineParser::is_token_char('1'));
403 assert!(StateMachineParser::is_token_char('_'));
404 assert!(!StateMachineParser::is_token_char('.'));
405 }
406}