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> {
185 match option {
187 "mode" => Ok("Mode".to_string()),
188 "component" => Ok("Component".to_string()),
189 "ctype" => Ok("Component-Type".to_string()),
190 "compression" => Ok("Compression".to_string()),
191 "repack" => Ok("Repack".to_string()),
192 "repacksuffix" => Ok("Repack-Suffix".to_string()),
193 "bare" => Ok("Bare".to_string()),
194 "user-agent" => Ok("User-Agent".to_string()),
195 "pasv" | "passive" => Ok("Passive".to_string()),
196 "active" | "nopasv" => Ok("Active".to_string()),
197 "unzipopt" => Ok("Unzip-Options".to_string()),
198 "decompress" => Ok("Decompress".to_string()),
199 "dversionmangle" => Ok("Debian-Version-Mangle".to_string()),
200 "uversionmangle" => Ok("Upstream-Version-Mangle".to_string()),
201 "downloadurlmangle" => Ok("Download-URL-Mangle".to_string()),
202 "filenamemangle" => Ok("Filename-Mangle".to_string()),
203 "pgpsigurlmangle" => Ok("PGP-Signature-URL-Mangle".to_string()),
204 "oversionmangle" => Ok("Original-Version-Mangle".to_string()),
205 "pagemangle" => Ok("Page-Mangle".to_string()),
206 "dirversionmangle" => Ok("Directory-Version-Mangle".to_string()),
207 "versionmangle" => Ok("Version-Mangle".to_string()),
208 "hrefdecode" => Ok("Href-Decode".to_string()),
209 "pgpmode" => Ok("PGP-Mode".to_string()),
210 "gitmode" => Ok("Git-Mode".to_string()),
211 "gitexport" => Ok("Git-Export".to_string()),
212 "pretty" => Ok("Pretty".to_string()),
213 "date" => Ok("Date".to_string()),
214 "searchmode" => Ok("Search-Mode".to_string()),
215 _ => Err(ConversionError::UnknownOption(option.to_string())),
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_simple_conversion() {
226 let v4_input = r#"version=4
227https://example.com/files .*/v?(\d+\.\d+)\.tar\.gz
228"#;
229
230 let v4_file: WatchFile = v4_input.parse().unwrap();
231 let v5_file = convert_to_v5(&v4_file).unwrap();
232
233 assert_eq!(v5_file.version(), 5);
234
235 let entries: Vec<_> = v5_file.entries().collect();
236 assert_eq!(entries.len(), 1);
237 assert_eq!(entries[0].url(), "https://example.com/files");
238 assert_eq!(
239 entries[0].matching_pattern().unwrap(),
240 Some(".*/v?(\\d+\\.\\d+)\\.tar\\.gz".to_string())
241 );
242 }
243
244 #[test]
245 fn test_conversion_with_options() {
246 let v4_input = r#"version=4
247opts=filenamemangle=s/.*\/(.*)/$1/,compression=xz https://example.com/files .*/v?(\d+)\.tar\.gz
248"#;
249
250 let v4_file: WatchFile = v4_input.parse().unwrap();
251 let v5_file = convert_to_v5(&v4_file).unwrap();
252
253 let entries: Vec<_> = v5_file.entries().collect();
254 assert_eq!(entries.len(), 1);
255
256 let entry = &entries[0];
257 assert_eq!(
258 entry.get_option("Filename-Mangle"),
259 Some("s/.*\\/(.*)/$1/".to_string())
260 );
261 assert_eq!(entry.get_option("Compression"), Some("xz".to_string()));
262 }
263
264 #[test]
265 fn test_conversion_with_comments() {
266 let v4_input = r#"# This is a comment about the package
268version=4
269opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/files .*/v?(\d+)\.tar\.gz
270"#;
271
272 let v4_file: WatchFile = v4_input.parse().unwrap();
273 let v5_file = convert_to_v5(&v4_file).unwrap();
274
275 let output = ToString::to_string(&v5_file);
276
277 let expected = "Version: 5
279
280# This is a comment about the package
281Source: https://example.com/files
282Matching-Pattern: .*/v?(\\d+)\\.tar\\.gz
283Filename-Mangle: s/.*\\/(.*)/$1/
284";
285 assert_eq!(output, expected);
286 }
287
288 #[test]
289 fn test_conversion_multiple_entries() {
290 let v4_input = r#"version=4
291https://example.com/repo1 .*/v?(\d+)\.tar\.gz
292https://example.com/repo2 .*/release-(\d+)\.tar\.gz
293"#;
294
295 let v4_file: WatchFile = v4_input.parse().unwrap();
296 let v5_file = convert_to_v5(&v4_file).unwrap();
297
298 let entries: Vec<_> = v5_file.entries().collect();
299 assert_eq!(entries.len(), 2);
300 assert_eq!(entries[0].url(), "https://example.com/repo1");
301 assert_eq!(entries[1].url(), "https://example.com/repo2");
302 }
303
304 #[test]
305 fn test_option_to_field_name() {
306 assert_eq!(option_to_field_name("mode").unwrap(), "Mode");
307 assert_eq!(
308 option_to_field_name("filenamemangle").unwrap(),
309 "Filename-Mangle"
310 );
311 assert_eq!(option_to_field_name("pgpmode").unwrap(), "PGP-Mode");
312 assert_eq!(option_to_field_name("user-agent").unwrap(), "User-Agent");
313 assert_eq!(option_to_field_name("compression").unwrap(), "Compression");
314 }
315
316 #[test]
317 fn test_option_to_field_name_unknown() {
318 let result = option_to_field_name("unknownoption");
319 assert!(result.is_err());
320 match result {
321 Err(ConversionError::UnknownOption(opt)) => {
322 assert_eq!(opt, "unknownoption");
323 }
324 _ => panic!("Expected UnknownOption error"),
325 }
326 }
327
328 #[test]
329 fn test_roundtrip_conversion() {
330 let v4_input = r#"version=4
331opts=compression=xz,component=foo https://example.com/files .*/(\d+)\.tar\.gz
332"#;
333
334 let v4_file: WatchFile = v4_input.parse().unwrap();
335 let v5_file = convert_to_v5(&v4_file).unwrap();
336
337 let v5_str = ToString::to_string(&v5_file);
339 let v5_reparsed: crate::deb822::WatchFile = v5_str.parse().unwrap();
340
341 let entries: Vec<_> = v5_reparsed.entries().collect();
342 assert_eq!(entries.len(), 1);
343 assert_eq!(entries[0].component(), Some("foo".to_string()));
344 }
345
346 #[test]
347 fn test_conversion_with_version_policy_and_script() {
348 let v4_input = r#"version=4
349https://example.com/files .*/v?(\d+)\.tar\.gz debian uupdate
350"#;
351
352 let v4_file: WatchFile = v4_input.parse().unwrap();
353 let v5_file = convert_to_v5(&v4_file).unwrap();
354
355 let entries: Vec<_> = v5_file.entries().collect();
356 assert_eq!(entries.len(), 1);
357
358 let entry = &entries[0];
359 assert_eq!(entry.url(), "https://example.com/files");
360 assert_eq!(
361 entry.version_policy().unwrap(),
362 Some(crate::VersionPolicy::Debian)
363 );
364 assert_eq!(entry.script(), Some("uupdate".to_string()));
365
366 let output = v5_file.to_string();
368 let expected = "Version: 5
369
370Source: https://example.com/files
371Matching-Pattern: .*/v?(\\d+)\\.tar\\.gz
372Version-Policy: debian
373Script: uupdate
374";
375 assert_eq!(output, expected);
376 }
377
378 #[test]
379 fn test_conversion_with_mangle_options() {
380 let v4_input = r#"version=4
381opts=uversionmangle=s/-/~/g,dversionmangle=s/\+dfsg// https://example.com/files .*/(\d+)\.tar\.gz
382"#;
383
384 let v4_file: WatchFile = v4_input.parse().unwrap();
385 let v5_file = convert_to_v5(&v4_file).unwrap();
386
387 let entries: Vec<_> = v5_file.entries().collect();
388 assert_eq!(entries.len(), 1);
389
390 let entry = &entries[0];
391 assert_eq!(
392 entry.get_option("Upstream-Version-Mangle"),
393 Some("s/-/~/g".to_string())
394 );
395 assert_eq!(
396 entry.get_option("Debian-Version-Mangle"),
397 Some("s/\\+dfsg//".to_string())
398 );
399
400 let output = v5_file.to_string();
402 let expected = "Version: 5
403
404Source: https://example.com/files
405Matching-Pattern: .*/(\\d+)\\.tar\\.gz
406Upstream-Version-Mangle: s/-/~/g
407Debian-Version-Mangle: s/\\+dfsg//
408";
409 assert_eq!(output, expected);
410 }
411
412 #[test]
413 fn test_conversion_with_comment_before_entry() {
414 let v4_input = concat!(
418 "version=4\n",
419 "# try also https://pypi.debian.net/tomoscan/watch\n",
420 "opts=uversionmangle=s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/ \\\n",
421 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))\n"
422 );
423
424 let v4_file: WatchFile = v4_input.parse().unwrap();
425 let v5_file = convert_to_v5(&v4_file).unwrap();
426
427 assert_eq!(v5_file.version(), 5);
428
429 let entries: Vec<_> = v5_file.entries().collect();
430 assert_eq!(entries.len(), 1);
431 assert_eq!(
432 entries[0].url(),
433 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))"
434 );
435 assert_eq!(
436 entries[0].get_option("Upstream-Version-Mangle"),
437 Some("s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/".to_string())
438 );
439 }
440}