1use crate::linebased::{Entry, WatchFile};
4use crate::SyntaxKind::*;
5use deb822_lossless::{Deb822, Paragraph};
6
7#[derive(Debug)]
9pub enum ConversionError {
10 UnknownOption(String),
12 InvalidVersionPolicy(String),
14}
15
16impl std::fmt::Display for ConversionError {
17 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
18 match self {
19 ConversionError::UnknownOption(opt) => {
20 write!(f, "Unknown option '{}' cannot be converted to v5", opt)
21 }
22 ConversionError::InvalidVersionPolicy(err) => {
23 write!(f, "Invalid version policy: {}", err)
24 }
25 }
26 }
27}
28
29impl std::error::Error for ConversionError {}
30
31pub fn convert_to_v5(watch_file: &WatchFile) -> Result<crate::deb822::WatchFile, ConversionError> {
36 let mut paragraphs = vec![vec![("Version", "5")].into_iter().collect()];
38
39 let leading_comments = extract_leading_comments(watch_file);
41
42 for _entry in watch_file.entries() {
44 let para: deb822_lossless::Paragraph =
45 vec![("Source", "placeholder")].into_iter().collect();
46 paragraphs.push(para);
47 }
48
49 let deb822: Deb822 = paragraphs.into_iter().collect();
50
51 let mut para_iter = deb822.paragraphs();
53 para_iter.next(); for (entry, mut para) in watch_file.entries().zip(para_iter) {
56 let entry_comments = extract_entry_comments(&entry);
58 for comment in entry_comments {
59 para.insert_comment_before(&comment);
60 }
61
62 convert_entry_to_v5(&entry, &mut para)?;
64 }
65
66 if !leading_comments.is_empty() {
68 if let Some(mut first_entry_para) = deb822.paragraphs().nth(1) {
69 for comment in leading_comments.iter().rev() {
70 first_entry_para.insert_comment_before(comment);
71 }
72 }
73 }
74
75 let output = deb822.to_string();
77 output
78 .parse()
79 .map_err(|_| ConversionError::UnknownOption("Failed to parse generated v5".to_string()))
80}
81
82fn extract_leading_comments(watch_file: &WatchFile) -> Vec<String> {
84 let mut comments = Vec::new();
85 let syntax = watch_file.syntax();
86
87 for child in syntax.children_with_tokens() {
88 match child {
89 rowan::NodeOrToken::Token(token) => {
90 if token.kind() == COMMENT {
91 let text = token.text();
94 let comment = text
95 .strip_prefix("# ")
96 .or_else(|| text.strip_prefix('#'))
97 .unwrap_or(text);
98 comments.push(comment.to_string());
99 }
100 }
101 rowan::NodeOrToken::Node(node) => {
102 if node.kind() == ENTRY {
104 break;
105 }
106 }
107 }
108 }
109
110 comments
111}
112
113fn extract_entry_comments(entry: &Entry) -> Vec<String> {
115 let mut comments = Vec::new();
116 let syntax = entry.syntax();
117
118 for child in syntax.children_with_tokens() {
120 if let rowan::NodeOrToken::Token(token) = child {
121 if token.kind() == COMMENT {
122 let text = token.text();
125 let comment = text
126 .strip_prefix("# ")
127 .or_else(|| text.strip_prefix('#'))
128 .unwrap_or(text);
129 comments.push(comment.to_string());
130 }
131 }
132 }
133
134 comments
135}
136
137fn convert_entry_to_v5(entry: &Entry, para: &mut Paragraph) -> Result<(), ConversionError> {
139 let url = entry.url();
141 if !url.is_empty() {
142 para.set("Source", &url);
143 }
144
145 if let Some(pattern) = entry.matching_pattern() {
147 para.set("Matching-Pattern", &pattern);
148 }
149
150 match entry.version() {
152 Ok(Some(version_policy)) => {
153 para.set("Version-Policy", &version_policy.to_string());
154 }
155 Err(err) => return Err(ConversionError::InvalidVersionPolicy(err)),
156 Ok(None) => {}
157 }
158
159 if let Some(script) = entry.script() {
161 para.set("Script", &script);
162 }
163
164 if let Some(opts_list) = entry.option_list() {
166 for (key, value) in opts_list.iter_key_values() {
167 let field_name = option_to_field_name(&key)?;
169 para.set(&field_name, &value);
170 }
171 }
172
173 Ok(())
174}
175
176fn option_to_field_name(option: &str) -> Result<String, ConversionError> {
194 match option {
196 "date" => return Ok("Git-Date".to_string()),
197 "pretty" => return Ok("Git-Pretty".to_string()),
198 _ => {}
199 }
200
201 match option {
203 "mode" => Ok("Mode".to_string()),
204 "component" => Ok("Component".to_string()),
205 "ctype" => Ok("Ctype".to_string()),
206 "compression" => Ok("Compression".to_string()),
207 "repack" => Ok("Repack".to_string()),
208 "repacksuffix" => Ok("Repacksuffix".to_string()),
209 "bare" => Ok("Bare".to_string()),
210 "user-agent" => Ok("User-Agent".to_string()),
211 "pasv" | "passive" => Ok("Passive".to_string()),
212 "active" | "nopasv" => Ok("Active".to_string()),
213 "unzipopt" => Ok("Unzipopt".to_string()),
214 "decompress" => Ok("Decompress".to_string()),
215 "dversionmangle" => Ok("Dversionmangle".to_string()),
216 "uversionmangle" => Ok("Uversionmangle".to_string()),
217 "downloadurlmangle" => Ok("Downloadurlmangle".to_string()),
218 "filenamemangle" => Ok("Filenamemangle".to_string()),
219 "pgpsigurlmangle" => Ok("Pgpsigurlmangle".to_string()),
220 "oversionmangle" => Ok("Oversionmangle".to_string()),
221 "pagemangle" => Ok("Pagemangle".to_string()),
222 "dirversionmangle" => Ok("Dirversionmangle".to_string()),
223 "versionmangle" => Ok("Versionmangle".to_string()),
224 "hrefdecode" => Ok("Hrefdecode".to_string()),
225 "pgpmode" => Ok("Pgpmode".to_string()),
226 "gitmode" => Ok("Gitmode".to_string()),
227 "gitexport" => Ok("Gitexport".to_string()),
228 "searchmode" => Ok("Searchmode".to_string()),
229 _ => Err(ConversionError::UnknownOption(option.to_string())),
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn test_simple_conversion() {
240 let v4_input = r#"version=4
241https://example.com/files .*/v?(\d+\.\d+)\.tar\.gz
242"#;
243
244 let v4_file: WatchFile = v4_input.parse().unwrap();
245 let v5_file = convert_to_v5(&v4_file).unwrap();
246
247 assert_eq!(v5_file.version(), 5);
248
249 let entries: Vec<_> = v5_file.entries().collect();
250 assert_eq!(entries.len(), 1);
251 assert_eq!(entries[0].url(), "https://example.com/files");
252 assert_eq!(
253 entries[0].matching_pattern().unwrap(),
254 Some(".*/v?(\\d+\\.\\d+)\\.tar\\.gz".to_string())
255 );
256 }
257
258 #[test]
259 fn test_conversion_with_options() {
260 let v4_input = r#"version=4
261opts=filenamemangle=s/.*\/(.*)/$1/,compression=xz https://example.com/files .*/v?(\d+)\.tar\.gz
262"#;
263
264 let v4_file: WatchFile = v4_input.parse().unwrap();
265 let v5_file = convert_to_v5(&v4_file).unwrap();
266
267 let entries: Vec<_> = v5_file.entries().collect();
268 assert_eq!(entries.len(), 1);
269
270 let entry = &entries[0];
271 assert_eq!(
272 entry.get_option("Filenamemangle"),
273 Some("s/.*\\/(.*)/$1/".to_string())
274 );
275 assert_eq!(entry.get_option("Compression"), Some("xz".to_string()));
276 }
277
278 #[test]
279 fn test_conversion_with_comments() {
280 let v4_input = r#"# This is a comment about the package
282version=4
283opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/files .*/v?(\d+)\.tar\.gz
284"#;
285
286 let v4_file: WatchFile = v4_input.parse().unwrap();
287 let v5_file = convert_to_v5(&v4_file).unwrap();
288
289 let output = ToString::to_string(&v5_file);
290
291 let expected = "Version: 5
293
294# This is a comment about the package
295Source: https://example.com/files
296Matching-Pattern: .*/v?(\\d+)\\.tar\\.gz
297Filenamemangle: s/.*\\/(.*)/$1/
298";
299 assert_eq!(output, expected);
300 }
301
302 #[test]
303 fn test_conversion_multiple_entries() {
304 let v4_input = r#"version=4
305https://example.com/repo1 .*/v?(\d+)\.tar\.gz
306https://example.com/repo2 .*/release-(\d+)\.tar\.gz
307"#;
308
309 let v4_file: WatchFile = v4_input.parse().unwrap();
310 let v5_file = convert_to_v5(&v4_file).unwrap();
311
312 let entries: Vec<_> = v5_file.entries().collect();
313 assert_eq!(entries.len(), 2);
314 assert_eq!(entries[0].url(), "https://example.com/repo1");
315 assert_eq!(entries[1].url(), "https://example.com/repo2");
316 }
317
318 #[test]
319 fn test_option_to_field_name() {
320 assert_eq!(option_to_field_name("mode").unwrap(), "Mode");
321 assert_eq!(
322 option_to_field_name("filenamemangle").unwrap(),
323 "Filenamemangle"
324 );
325 assert_eq!(option_to_field_name("pgpmode").unwrap(), "Pgpmode");
326 assert_eq!(option_to_field_name("user-agent").unwrap(), "User-Agent");
327 assert_eq!(option_to_field_name("compression").unwrap(), "Compression");
328 assert_eq!(option_to_field_name("date").unwrap(), "Git-Date");
329 assert_eq!(option_to_field_name("pretty").unwrap(), "Git-Pretty");
330 }
331
332 #[test]
333 fn test_option_to_field_name_unknown() {
334 let result = option_to_field_name("unknownoption");
335 assert!(result.is_err());
336 match result {
337 Err(ConversionError::UnknownOption(opt)) => {
338 assert_eq!(opt, "unknownoption");
339 }
340 _ => panic!("Expected UnknownOption error"),
341 }
342 }
343
344 #[test]
345 fn test_roundtrip_conversion() {
346 let v4_input = r#"version=4
347opts=compression=xz,component=foo https://example.com/files .*/(\d+)\.tar\.gz
348"#;
349
350 let v4_file: WatchFile = v4_input.parse().unwrap();
351 let v5_file = convert_to_v5(&v4_file).unwrap();
352
353 let v5_str = ToString::to_string(&v5_file);
355 let v5_reparsed: crate::deb822::WatchFile = v5_str.parse().unwrap();
356
357 let entries: Vec<_> = v5_reparsed.entries().collect();
358 assert_eq!(entries.len(), 1);
359 assert_eq!(entries[0].component(), Some("foo".to_string()));
360 }
361
362 #[test]
363 fn test_conversion_with_version_policy_and_script() {
364 let v4_input = r#"version=4
365https://example.com/files .*/v?(\d+)\.tar\.gz debian uupdate
366"#;
367
368 let v4_file: WatchFile = v4_input.parse().unwrap();
369 let v5_file = convert_to_v5(&v4_file).unwrap();
370
371 let entries: Vec<_> = v5_file.entries().collect();
372 assert_eq!(entries.len(), 1);
373
374 let entry = &entries[0];
375 assert_eq!(entry.url(), "https://example.com/files");
376 assert_eq!(
377 entry.version_policy().unwrap(),
378 Some(crate::VersionPolicy::Debian)
379 );
380 assert_eq!(entry.script(), Some("uupdate".to_string()));
381
382 let output = v5_file.to_string();
384 let expected = "Version: 5
385
386Source: https://example.com/files
387Matching-Pattern: .*/v?(\\d+)\\.tar\\.gz
388Version-Policy: debian
389Script: uupdate
390";
391 assert_eq!(output, expected);
392 }
393
394 #[test]
395 fn test_conversion_with_mangle_options() {
396 let v4_input = r#"version=4
397opts=uversionmangle=s/-/~/g,dversionmangle=s/\+dfsg// https://example.com/files .*/(\d+)\.tar\.gz
398"#;
399
400 let v4_file: WatchFile = v4_input.parse().unwrap();
401 let v5_file = convert_to_v5(&v4_file).unwrap();
402
403 let entries: Vec<_> = v5_file.entries().collect();
404 assert_eq!(entries.len(), 1);
405
406 let entry = &entries[0];
407 assert_eq!(
408 entry.get_option("Uversionmangle"),
409 Some("s/-/~/g".to_string())
410 );
411 assert_eq!(
412 entry.get_option("Dversionmangle"),
413 Some("s/\\+dfsg//".to_string())
414 );
415
416 let output = v5_file.to_string();
418 let expected = "Version: 5
419
420Source: https://example.com/files
421Matching-Pattern: .*/(\\d+)\\.tar\\.gz
422Uversionmangle: s/-/~/g
423Dversionmangle: s/\\+dfsg//
424";
425 assert_eq!(output, expected);
426 }
427
428 #[test]
429 fn test_conversion_with_comment_before_entry() {
430 let v4_input = concat!(
434 "version=4\n",
435 "# try also https://pypi.debian.net/tomoscan/watch\n",
436 "opts=uversionmangle=s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/ \\\n",
437 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))\n"
438 );
439
440 let v4_file: WatchFile = v4_input.parse().unwrap();
441 let v5_file = convert_to_v5(&v4_file).unwrap();
442
443 assert_eq!(v5_file.version(), 5);
444
445 let entries: Vec<_> = v5_file.entries().collect();
446 assert_eq!(entries.len(), 1);
447 assert_eq!(
448 entries[0].url(),
449 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))"
450 );
451 assert_eq!(
452 entries[0].get_option("Uversionmangle"),
453 Some("s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/".to_string())
454 );
455 }
456}