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[1 + delimiter.len_utf8()..];
113
114 let parts = split_by_unescaped_delimiter(rest, delimiter);
116
117 if parts.len() < 2 {
118 return Err(MangleError::InvalidSubstExpr(
119 "not enough parts".to_string(),
120 ));
121 }
122
123 let pattern = parts[0].clone();
124 let replacement = parts[1].clone();
125 let flags = if parts.len() > 2 && !parts[2].is_empty() {
126 Some(parts[2].clone())
127 } else {
128 None
129 };
130
131 Ok(MangleExpr {
132 kind: MangleExprKind::Subst,
133 pattern,
134 replacement,
135 flags,
136 })
137}
138
139pub fn parse_transl_expr(vm: &str) -> Result<MangleExpr, MangleError> {
151 let rest = if vm.starts_with("tr") {
152 &vm[2..]
153 } else if vm.starts_with('y') {
154 &vm[1..]
155 } else {
156 return Err(MangleError::InvalidTranslExpr(
157 "not a translation expression".to_string(),
158 ));
159 };
160
161 if rest.is_empty() {
162 return Err(MangleError::InvalidTranslExpr(
163 "expression too short".to_string(),
164 ));
165 }
166
167 let delimiter = rest.chars().next().unwrap();
168 let rest = &rest[delimiter.len_utf8()..];
169
170 let parts = split_by_unescaped_delimiter(rest, delimiter);
172
173 if parts.len() < 2 {
174 return Err(MangleError::InvalidTranslExpr(
175 "not enough parts".to_string(),
176 ));
177 }
178
179 let pattern = parts[0].clone();
180 let replacement = parts[1].clone();
181 let flags = if parts.len() > 2 && !parts[2].is_empty() {
182 Some(parts[2].clone())
183 } else {
184 None
185 };
186
187 Ok(MangleExpr {
188 kind: MangleExprKind::Transl,
189 pattern,
190 replacement,
191 flags,
192 })
193}
194
195fn split_by_unescaped_delimiter(s: &str, delimiter: char) -> Vec<String> {
197 let mut parts = Vec::new();
198 let mut current = String::new();
199 let mut escaped = false;
200
201 for c in s.chars() {
202 if escaped {
203 current.push(c);
204 escaped = false;
205 } else if c == '\\' {
206 current.push(c);
207 escaped = true;
208 } else if c == delimiter {
209 parts.push(current.clone());
210 current.clear();
211 } else {
212 current.push(c);
213 }
214 }
215
216 parts.push(current);
218
219 parts
220}
221
222pub fn apply_mangle(vm: &str, orig: &str) -> Result<String, MangleError> {
236 let expr = parse_mangle_expr(vm)?;
237
238 match expr.kind {
239 MangleExprKind::Subst => {
240 let re =
241 Regex::new(&expr.pattern).map_err(|e| MangleError::RegexError(e.to_string()))?;
242
243 let global = expr.flags.as_ref().is_some_and(|f| f.contains('g'));
245
246 if global {
247 Ok(re.replace_all(orig, expr.replacement.as_str()).to_string())
248 } else {
249 Ok(re.replace(orig, expr.replacement.as_str()).to_string())
250 }
251 }
252 MangleExprKind::Transl => {
253 apply_translation(&expr.pattern, &expr.replacement, orig)
255 }
256 }
257}
258
259pub fn apply_mangle_with_subst(
278 vm: &str,
279 orig: &str,
280 package: impl FnOnce() -> String,
281 component: impl FnOnce() -> String,
282) -> Result<String, MangleError> {
283 let substituted_vm = crate::subst::subst(vm, package, component);
285
286 apply_mangle(&substituted_vm, orig)
288}
289
290fn apply_translation(pattern: &str, replacement: &str, orig: &str) -> Result<String, MangleError> {
292 let from_chars = expand_char_range(pattern);
294 let to_chars = expand_char_range(replacement);
295
296 if from_chars.len() != to_chars.len() {
297 return Err(MangleError::InvalidTranslExpr(
298 "pattern and replacement must have same length".to_string(),
299 ));
300 }
301
302 let mut result = String::new();
303 for c in orig.chars() {
304 if let Some(pos) = from_chars.iter().position(|&fc| fc == c) {
305 result.push(to_chars[pos]);
306 } else {
307 result.push(c);
308 }
309 }
310
311 Ok(result)
312}
313
314fn expand_char_range(s: &str) -> Vec<char> {
316 let mut result = Vec::new();
317 let chars: Vec<char> = s.chars().collect();
318 let mut i = 0;
319
320 while i < chars.len() {
321 if i + 2 < chars.len() && chars[i + 1] == '-' {
322 let start = chars[i];
324 let end = chars[i + 2];
325 for c in (start as u32)..=(end as u32) {
326 if let Some(ch) = char::from_u32(c) {
327 result.push(ch);
328 }
329 }
330 i += 3;
331 } else {
332 result.push(chars[i]);
333 i += 1;
334 }
335 }
336
337 result
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn test_parse_subst_expr() {
346 let expr = parse_subst_expr("s/foo/bar/g").unwrap();
347 assert_eq!(expr.pattern, "foo");
348 assert_eq!(expr.replacement, "bar");
349 assert_eq!(expr.flags.as_deref(), Some("g"));
350
351 let expr = parse_subst_expr("s|foo|bar|").unwrap();
352 assert_eq!(expr.pattern, "foo");
353 assert_eq!(expr.replacement, "bar");
354 assert_eq!(expr.flags, None);
355
356 let expr = parse_subst_expr("s#a/b#c/d#").unwrap();
357 assert_eq!(expr.pattern, "a/b");
358 assert_eq!(expr.replacement, "c/d");
359 }
360
361 #[test]
362 fn test_parse_transl_expr() {
363 let expr = parse_transl_expr("tr/a-z/A-Z/").unwrap();
364 assert_eq!(expr.pattern, "a-z");
365 assert_eq!(expr.replacement, "A-Z");
366
367 let expr = parse_transl_expr("y/abc/xyz/").unwrap();
368 assert_eq!(expr.pattern, "abc");
369 assert_eq!(expr.replacement, "xyz");
370 }
371
372 #[test]
373 fn test_apply_mangle_subst() {
374 let result = apply_mangle("s/foo/bar/", "foo baz foo").unwrap();
375 assert_eq!(result, "bar baz foo");
376
377 let result = apply_mangle("s/foo/bar/g", "foo baz foo").unwrap();
378 assert_eq!(result, "bar baz bar");
379
380 let result = apply_mangle("s/[0-9]+/X/g", "a1b2c3").unwrap();
382 assert_eq!(result, "aXbXcX");
383 }
384
385 #[test]
386 fn test_apply_mangle_transl() {
387 let result = apply_mangle("tr/a-z/A-Z/", "hello").unwrap();
388 assert_eq!(result, "HELLO");
389
390 let result = apply_mangle("y/abc/xyz/", "aabbcc").unwrap();
391 assert_eq!(result, "xxyyzz");
392 }
393
394 #[test]
395 fn test_expand_char_range() {
396 let result = expand_char_range("a-z");
397 assert_eq!(result.len(), 26);
398 assert_eq!(result[0], 'a');
399 assert_eq!(result[25], 'z');
400
401 let result = expand_char_range("a-c");
402 assert_eq!(result, vec!['a', 'b', 'c']);
403
404 let result = expand_char_range("abc");
405 assert_eq!(result, vec!['a', 'b', 'c']);
406 }
407
408 #[test]
409 fn test_split_by_unescaped_delimiter() {
410 let result = split_by_unescaped_delimiter("foo/bar/baz", '/');
411 assert_eq!(result, vec!["foo", "bar", "baz"]);
412
413 let result = split_by_unescaped_delimiter("foo\\/bar/baz", '/');
414 assert_eq!(result, vec!["foo\\/bar", "baz"]);
415 }
416
417 #[test]
418 fn test_real_world_examples() {
419 let result = apply_mangle(r"s/\+ds//", "1.0+ds").unwrap();
421 assert_eq!(result, "1.0");
422
423 let result = apply_mangle(
425 r"s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1.tar.gz/",
426 "https://github.com/syncthing/syncthing-gtk/archive/v0.9.4.tar.gz",
427 )
428 .unwrap();
429 assert_eq!(result, "syncthing-gtk-0.9.4.tar.gz");
430 }
431
432 #[test]
433 fn test_apply_mangle_with_subst_package() {
434 let result = apply_mangle_with_subst(
437 "s/@PACKAGE@/replaced/",
438 "foo mypackage bar",
439 || "mypackage".to_string(),
440 || String::new(),
441 )
442 .unwrap();
443 assert_eq!(result, "foo replaced bar");
444 }
445
446 #[test]
447 fn test_apply_mangle_with_subst_component() {
448 let result = apply_mangle_with_subst(
451 "s/@COMPONENT@/replaced/g",
452 "upstream foo upstream",
453 || unreachable!(),
454 || "upstream".to_string(),
455 )
456 .unwrap();
457 assert_eq!(result, "replaced foo replaced");
458 }
459
460 #[test]
461 fn test_apply_mangle_with_subst_filenamemangle() {
462 let result = apply_mangle_with_subst(
464 r"s/.+\/v?(\d\S+)\.tar\.gz/@PACKAGE@-$1.tar.gz/",
465 "https://github.com/example/repo/archive/v0.9.4.tar.gz",
466 || "myapp".to_string(),
467 || String::new(),
468 )
469 .unwrap();
470 assert_eq!(result, "myapp-0.9.4.tar.gz");
471 }
472
473 #[test]
474 fn test_apply_mangle_with_subst_no_templates() {
475 let result = apply_mangle_with_subst(
477 "s/foo/bar/g",
478 "foo baz foo",
479 || unreachable!(),
480 || unreachable!(),
481 )
482 .unwrap();
483 assert_eq!(result, "bar baz bar");
484 }
485
486 #[test]
487 fn test_multibyte_delimiter_does_not_panic() {
488 assert!(parse_subst_expr("s\u{383}foo\u{383}bar\u{383}").is_ok());
491 assert!(parse_transl_expr("tr\u{7d5}]").is_err());
492 let _ = parse_subst_expr("s\u{383}");
494 }
495}