1use regex::Regex;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum MangleError {
10 NotMangleExpr(String),
12 InvalidSubstExpr(String),
14 InvalidTranslExpr(String),
16 RegexError(String),
18}
19
20impl std::fmt::Display for MangleError {
21 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
22 match self {
23 MangleError::NotMangleExpr(s) => {
24 write!(f, "not a substitution or translation expression: {}", s)
25 }
26 MangleError::InvalidSubstExpr(s) => write!(f, "invalid substitution expression: {}", s),
27 MangleError::InvalidTranslExpr(s) => write!(f, "invalid translation expression: {}", s),
28 MangleError::RegexError(s) => write!(f, "regex error: {}", s),
29 }
30 }
31}
32
33impl std::error::Error for MangleError {}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum MangleExprKind {
38 Subst,
40 Transl,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct MangleExpr {
47 pub kind: MangleExprKind,
49 pub pattern: String,
51 pub replacement: String,
53 pub flags: Option<String>,
55}
56
57pub fn parse_mangle_expr(vm: &str) -> Result<MangleExpr, MangleError> {
70 if vm.starts_with('s') {
71 parse_subst_expr(vm)
72 } else if vm.starts_with("tr") {
73 parse_transl_expr(vm)
74 } else if vm.starts_with('y') {
75 parse_transl_expr(vm)
76 } else {
77 Err(MangleError::NotMangleExpr(vm.to_string()))
78 }
79}
80
81pub fn parse_subst_expr(vm: &str) -> Result<MangleExpr, MangleError> {
98 if !vm.starts_with('s') {
99 return Err(MangleError::InvalidSubstExpr(
100 "not a substitution expression".to_string(),
101 ));
102 }
103
104 if vm.len() < 2 {
105 return Err(MangleError::InvalidSubstExpr(
106 "expression too short".to_string(),
107 ));
108 }
109
110 let delimiter = vm.chars().nth(1).unwrap();
111 let rest = &vm[2..];
112
113 let parts = split_by_unescaped_delimiter(rest, delimiter);
115
116 if parts.len() < 2 {
117 return Err(MangleError::InvalidSubstExpr(
118 "not enough parts".to_string(),
119 ));
120 }
121
122 let pattern = parts[0].clone();
123 let replacement = parts[1].clone();
124 let flags = if parts.len() > 2 && !parts[2].is_empty() {
125 Some(parts[2].clone())
126 } else {
127 None
128 };
129
130 Ok(MangleExpr {
131 kind: MangleExprKind::Subst,
132 pattern,
133 replacement,
134 flags,
135 })
136}
137
138pub fn parse_transl_expr(vm: &str) -> Result<MangleExpr, MangleError> {
150 let rest = if vm.starts_with("tr") {
151 &vm[2..]
152 } else if vm.starts_with('y') {
153 &vm[1..]
154 } else {
155 return Err(MangleError::InvalidTranslExpr(
156 "not a translation expression".to_string(),
157 ));
158 };
159
160 if rest.is_empty() {
161 return Err(MangleError::InvalidTranslExpr(
162 "expression too short".to_string(),
163 ));
164 }
165
166 let delimiter = rest.chars().next().unwrap();
167 let rest = &rest[1..];
168
169 let parts = split_by_unescaped_delimiter(rest, delimiter);
171
172 if parts.len() < 2 {
173 return Err(MangleError::InvalidTranslExpr(
174 "not enough parts".to_string(),
175 ));
176 }
177
178 let pattern = parts[0].clone();
179 let replacement = parts[1].clone();
180 let flags = if parts.len() > 2 && !parts[2].is_empty() {
181 Some(parts[2].clone())
182 } else {
183 None
184 };
185
186 Ok(MangleExpr {
187 kind: MangleExprKind::Transl,
188 pattern,
189 replacement,
190 flags,
191 })
192}
193
194fn split_by_unescaped_delimiter(s: &str, delimiter: char) -> Vec<String> {
196 let mut parts = Vec::new();
197 let mut current = String::new();
198 let mut escaped = false;
199
200 for c in s.chars() {
201 if escaped {
202 current.push(c);
203 escaped = false;
204 } else if c == '\\' {
205 current.push(c);
206 escaped = true;
207 } else if c == delimiter {
208 parts.push(current.clone());
209 current.clear();
210 } else {
211 current.push(c);
212 }
213 }
214
215 parts.push(current);
217
218 parts
219}
220
221pub fn apply_mangle(vm: &str, orig: &str) -> Result<String, MangleError> {
235 let expr = parse_mangle_expr(vm)?;
236
237 match expr.kind {
238 MangleExprKind::Subst => {
239 let re =
240 Regex::new(&expr.pattern).map_err(|e| MangleError::RegexError(e.to_string()))?;
241
242 let global = expr.flags.as_ref().is_some_and(|f| f.contains('g'));
244
245 if global {
246 Ok(re.replace_all(orig, expr.replacement.as_str()).to_string())
247 } else {
248 Ok(re.replace(orig, expr.replacement.as_str()).to_string())
249 }
250 }
251 MangleExprKind::Transl => {
252 apply_translation(&expr.pattern, &expr.replacement, orig)
254 }
255 }
256}
257
258pub fn apply_mangle_with_subst(
277 vm: &str,
278 orig: &str,
279 package: impl FnOnce() -> String,
280 component: impl FnOnce() -> String,
281) -> Result<String, MangleError> {
282 let substituted_vm = crate::subst::subst(vm, package, component);
284
285 apply_mangle(&substituted_vm, orig)
287}
288
289fn apply_translation(pattern: &str, replacement: &str, orig: &str) -> Result<String, MangleError> {
291 let from_chars = expand_char_range(pattern);
293 let to_chars = expand_char_range(replacement);
294
295 if from_chars.len() != to_chars.len() {
296 return Err(MangleError::InvalidTranslExpr(
297 "pattern and replacement must have same length".to_string(),
298 ));
299 }
300
301 let mut result = String::new();
302 for c in orig.chars() {
303 if let Some(pos) = from_chars.iter().position(|&fc| fc == c) {
304 result.push(to_chars[pos]);
305 } else {
306 result.push(c);
307 }
308 }
309
310 Ok(result)
311}
312
313fn expand_char_range(s: &str) -> Vec<char> {
315 let mut result = Vec::new();
316 let chars: Vec<char> = s.chars().collect();
317 let mut i = 0;
318
319 while i < chars.len() {
320 if i + 2 < chars.len() && chars[i + 1] == '-' {
321 let start = chars[i];
323 let end = chars[i + 2];
324 for c in (start as u32)..=(end as u32) {
325 if let Some(ch) = char::from_u32(c) {
326 result.push(ch);
327 }
328 }
329 i += 3;
330 } else {
331 result.push(chars[i]);
332 i += 1;
333 }
334 }
335
336 result
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn test_parse_subst_expr() {
345 let expr = parse_subst_expr("s/foo/bar/g").unwrap();
346 assert_eq!(expr.pattern, "foo");
347 assert_eq!(expr.replacement, "bar");
348 assert_eq!(expr.flags.as_deref(), Some("g"));
349
350 let expr = parse_subst_expr("s|foo|bar|").unwrap();
351 assert_eq!(expr.pattern, "foo");
352 assert_eq!(expr.replacement, "bar");
353 assert_eq!(expr.flags, None);
354
355 let expr = parse_subst_expr("s#a/b#c/d#").unwrap();
356 assert_eq!(expr.pattern, "a/b");
357 assert_eq!(expr.replacement, "c/d");
358 }
359
360 #[test]
361 fn test_parse_transl_expr() {
362 let expr = parse_transl_expr("tr/a-z/A-Z/").unwrap();
363 assert_eq!(expr.pattern, "a-z");
364 assert_eq!(expr.replacement, "A-Z");
365
366 let expr = parse_transl_expr("y/abc/xyz/").unwrap();
367 assert_eq!(expr.pattern, "abc");
368 assert_eq!(expr.replacement, "xyz");
369 }
370
371 #[test]
372 fn test_apply_mangle_subst() {
373 let result = apply_mangle("s/foo/bar/", "foo baz foo").unwrap();
374 assert_eq!(result, "bar baz foo");
375
376 let result = apply_mangle("s/foo/bar/g", "foo baz foo").unwrap();
377 assert_eq!(result, "bar baz bar");
378
379 let result = apply_mangle("s/[0-9]+/X/g", "a1b2c3").unwrap();
381 assert_eq!(result, "aXbXcX");
382 }
383
384 #[test]
385 fn test_apply_mangle_transl() {
386 let result = apply_mangle("tr/a-z/A-Z/", "hello").unwrap();
387 assert_eq!(result, "HELLO");
388
389 let result = apply_mangle("y/abc/xyz/", "aabbcc").unwrap();
390 assert_eq!(result, "xxyyzz");
391 }
392
393 #[test]
394 fn test_expand_char_range() {
395 let result = expand_char_range("a-z");
396 assert_eq!(result.len(), 26);
397 assert_eq!(result[0], 'a');
398 assert_eq!(result[25], 'z');
399
400 let result = expand_char_range("a-c");
401 assert_eq!(result, vec!['a', 'b', 'c']);
402
403 let result = expand_char_range("abc");
404 assert_eq!(result, vec!['a', 'b', 'c']);
405 }
406
407 #[test]
408 fn test_split_by_unescaped_delimiter() {
409 let result = split_by_unescaped_delimiter("foo/bar/baz", '/');
410 assert_eq!(result, vec!["foo", "bar", "baz"]);
411
412 let result = split_by_unescaped_delimiter("foo\\/bar/baz", '/');
413 assert_eq!(result, vec!["foo\\/bar", "baz"]);
414 }
415
416 #[test]
417 fn test_real_world_examples() {
418 let result = apply_mangle(r"s/\+ds//", "1.0+ds").unwrap();
420 assert_eq!(result, "1.0");
421
422 let result = apply_mangle(
424 r"s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1.tar.gz/",
425 "https://github.com/syncthing/syncthing-gtk/archive/v0.9.4.tar.gz",
426 )
427 .unwrap();
428 assert_eq!(result, "syncthing-gtk-0.9.4.tar.gz");
429 }
430
431 #[test]
432 fn test_apply_mangle_with_subst_package() {
433 let result = apply_mangle_with_subst(
436 "s/@PACKAGE@/replaced/",
437 "foo mypackage bar",
438 || "mypackage".to_string(),
439 || String::new(),
440 )
441 .unwrap();
442 assert_eq!(result, "foo replaced bar");
443 }
444
445 #[test]
446 fn test_apply_mangle_with_subst_component() {
447 let result = apply_mangle_with_subst(
450 "s/@COMPONENT@/replaced/g",
451 "upstream foo upstream",
452 || unreachable!(),
453 || "upstream".to_string(),
454 )
455 .unwrap();
456 assert_eq!(result, "replaced foo replaced");
457 }
458
459 #[test]
460 fn test_apply_mangle_with_subst_filenamemangle() {
461 let result = apply_mangle_with_subst(
463 r"s/.+\/v?(\d\S+)\.tar\.gz/@PACKAGE@-$1.tar.gz/",
464 "https://github.com/example/repo/archive/v0.9.4.tar.gz",
465 || "myapp".to_string(),
466 || String::new(),
467 )
468 .unwrap();
469 assert_eq!(result, "myapp-0.9.4.tar.gz");
470 }
471
472 #[test]
473 fn test_apply_mangle_with_subst_no_templates() {
474 let result = apply_mangle_with_subst(
476 "s/foo/bar/g",
477 "foo baz foo",
478 || unreachable!(),
479 || unreachable!(),
480 )
481 .unwrap();
482 assert_eq!(result, "bar baz bar");
483 }
484}