1use crate::parse::{Entry, WatchFile};
4use crate::parse_v5::WatchFileV5;
5use crate::SyntaxKind::*;
6use deb822_lossless::{Deb822, Paragraph};
7
8#[derive(Debug)]
10pub enum ConversionError {
11 UnknownOption(String),
13}
14
15impl std::fmt::Display for ConversionError {
16 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
17 match self {
18 ConversionError::UnknownOption(opt) => {
19 write!(f, "Unknown option '{}' cannot be converted to v5", opt)
20 }
21 }
22 }
23}
24
25impl std::error::Error for ConversionError {}
26
27pub fn convert_to_v5(watch_file: &WatchFile) -> Result<WatchFileV5, ConversionError> {
32 let mut paragraphs = vec![vec![("Version", "5")].into_iter().collect()];
34
35 let leading_comments = extract_leading_comments(watch_file);
37
38 for _entry in watch_file.entries() {
40 let para: deb822_lossless::Paragraph =
41 vec![("Source", "placeholder")].into_iter().collect();
42 paragraphs.push(para);
43 }
44
45 let deb822: Deb822 = paragraphs.into_iter().collect();
46
47 let mut para_iter = deb822.paragraphs();
49 para_iter.next(); for (entry, mut para) in watch_file.entries().zip(para_iter) {
52 let entry_comments = extract_entry_comments(&entry);
54 for comment in entry_comments {
55 para.insert_comment_before(&comment);
56 }
57
58 convert_entry_to_v5(&entry, &mut para)?;
60 }
61
62 if !leading_comments.is_empty() {
64 if let Some(mut first_entry_para) = deb822.paragraphs().nth(1) {
65 for comment in leading_comments.iter().rev() {
66 first_entry_para.insert_comment_before(comment);
67 }
68 }
69 }
70
71 let output = deb822.to_string();
73 output
74 .parse()
75 .map_err(|_| ConversionError::UnknownOption("Failed to parse generated v5".to_string()))
76}
77
78fn extract_leading_comments(watch_file: &WatchFile) -> Vec<String> {
80 let mut comments = Vec::new();
81 let syntax = watch_file.syntax();
82
83 for child in syntax.children_with_tokens() {
84 match child {
85 rowan::NodeOrToken::Token(token) => {
86 if token.kind() == COMMENT {
87 let text = token.text();
90 let comment = text
91 .strip_prefix("# ")
92 .or_else(|| text.strip_prefix('#'))
93 .unwrap_or(text);
94 comments.push(comment.to_string());
95 }
96 }
97 rowan::NodeOrToken::Node(node) => {
98 if node.kind() == ENTRY {
100 break;
101 }
102 }
103 }
104 }
105
106 comments
107}
108
109fn extract_entry_comments(entry: &Entry) -> Vec<String> {
111 let mut comments = Vec::new();
112 let syntax = entry.syntax();
113
114 for child in syntax.children_with_tokens() {
116 if let rowan::NodeOrToken::Token(token) = child {
117 if token.kind() == COMMENT {
118 let text = token.text();
121 let comment = text
122 .strip_prefix("# ")
123 .or_else(|| text.strip_prefix('#'))
124 .unwrap_or(text);
125 comments.push(comment.to_string());
126 }
127 }
128 }
129
130 comments
131}
132
133fn convert_entry_to_v5(entry: &Entry, para: &mut Paragraph) -> Result<(), ConversionError> {
135 let url = entry.url();
137 if !url.is_empty() {
138 para.set("Source", &url);
139 }
140
141 if let Some(pattern) = entry.matching_pattern() {
143 para.set("Matching-Pattern", &pattern);
144 }
145
146 if let Ok(Some(version_policy)) = entry.version() {
148 para.set("Version-Policy", &version_policy.to_string());
149 }
150
151 if let Some(script) = entry.script() {
153 para.set("Script", &script);
154 }
155
156 if let Some(opts_list) = entry.option_list() {
158 for (key, value) in opts_list.options() {
159 let field_name = option_to_field_name(&key)?;
161 para.set(&field_name, &value);
162 }
163 }
164
165 Ok(())
166}
167
168fn option_to_field_name(option: &str) -> Result<String, ConversionError> {
177 match option {
179 "mode" => Ok("Mode".to_string()),
180 "component" => Ok("Component".to_string()),
181 "ctype" => Ok("Component-Type".to_string()),
182 "compression" => Ok("Compression".to_string()),
183 "repack" => Ok("Repack".to_string()),
184 "repacksuffix" => Ok("Repack-Suffix".to_string()),
185 "bare" => Ok("Bare".to_string()),
186 "user-agent" => Ok("User-Agent".to_string()),
187 "pasv" | "passive" => Ok("Passive".to_string()),
188 "active" | "nopasv" => Ok("Active".to_string()),
189 "unzipopt" => Ok("Unzip-Options".to_string()),
190 "decompress" => Ok("Decompress".to_string()),
191 "dversionmangle" => Ok("Debian-Version-Mangle".to_string()),
192 "uversionmangle" => Ok("Upstream-Version-Mangle".to_string()),
193 "downloadurlmangle" => Ok("Download-URL-Mangle".to_string()),
194 "filenamemangle" => Ok("Filename-Mangle".to_string()),
195 "pgpsigurlmangle" => Ok("PGP-Signature-URL-Mangle".to_string()),
196 "oversionmangle" => Ok("Original-Version-Mangle".to_string()),
197 "pagemangle" => Ok("Page-Mangle".to_string()),
198 "dirversionmangle" => Ok("Directory-Version-Mangle".to_string()),
199 "versionmangle" => Ok("Version-Mangle".to_string()),
200 "hrefdecode" => Ok("Href-Decode".to_string()),
201 "pgpmode" => Ok("PGP-Mode".to_string()),
202 "gitmode" => Ok("Git-Mode".to_string()),
203 "gitexport" => Ok("Git-Export".to_string()),
204 "pretty" => Ok("Pretty".to_string()),
205 "date" => Ok("Date".to_string()),
206 "searchmode" => Ok("Search-Mode".to_string()),
207 _ => Err(ConversionError::UnknownOption(option.to_string())),
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::traits::WatchEntry;
216
217 #[test]
218 fn test_simple_conversion() {
219 let v4_input = r#"version=4
220https://example.com/files .*/v?(\d+\.\d+)\.tar\.gz
221"#;
222
223 let v4_file: WatchFile = v4_input.parse().unwrap();
224 let v5_file = convert_to_v5(&v4_file).unwrap();
225
226 assert_eq!(v5_file.version(), 5);
227
228 let entries: Vec<_> = v5_file.entries().collect();
229 assert_eq!(entries.len(), 1);
230 assert_eq!(entries[0].url(), "https://example.com/files");
231 assert_eq!(
232 entries[0].matching_pattern(),
233 Some(".*/v?(\\d+\\.\\d+)\\.tar\\.gz".to_string())
234 );
235 }
236
237 #[test]
238 fn test_conversion_with_options() {
239 let v4_input = r#"version=4
240opts=filenamemangle=s/.*\/(.*)/$1/,compression=xz https://example.com/files .*/v?(\d+)\.tar\.gz
241"#;
242
243 let v4_file: WatchFile = v4_input.parse().unwrap();
244 let v5_file = convert_to_v5(&v4_file).unwrap();
245
246 let entries: Vec<_> = v5_file.entries().collect();
247 assert_eq!(entries.len(), 1);
248
249 let entry = &entries[0];
250 assert_eq!(
251 entry.get_option("Filename-Mangle"),
252 Some("s/.*\\/(.*)/$1/".to_string())
253 );
254 assert_eq!(entry.get_option("Compression"), Some("xz".to_string()));
255 }
256
257 #[test]
258 fn test_conversion_with_comments() {
259 let v4_input = r#"# This is a comment about the package
261version=4
262opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/files .*/v?(\d+)\.tar\.gz
263"#;
264
265 let v4_file: WatchFile = v4_input.parse().unwrap();
266 let v5_file = convert_to_v5(&v4_file).unwrap();
267
268 let output = ToString::to_string(&v5_file);
269
270 assert!(output.contains("# This is a comment about the package"));
272 assert!(output.contains("Version: 5"));
273 }
274
275 #[test]
276 fn test_conversion_multiple_entries() {
277 let v4_input = r#"version=4
278https://example.com/repo1 .*/v?(\d+)\.tar\.gz
279https://example.com/repo2 .*/release-(\d+)\.tar\.gz
280"#;
281
282 let v4_file: WatchFile = v4_input.parse().unwrap();
283 let v5_file = convert_to_v5(&v4_file).unwrap();
284
285 let entries: Vec<_> = v5_file.entries().collect();
286 assert_eq!(entries.len(), 2);
287 assert_eq!(entries[0].url(), "https://example.com/repo1");
288 assert_eq!(entries[1].url(), "https://example.com/repo2");
289 }
290
291 #[test]
292 fn test_option_to_field_name() {
293 assert_eq!(option_to_field_name("mode").unwrap(), "Mode");
294 assert_eq!(
295 option_to_field_name("filenamemangle").unwrap(),
296 "Filename-Mangle"
297 );
298 assert_eq!(option_to_field_name("pgpmode").unwrap(), "PGP-Mode");
299 assert_eq!(option_to_field_name("user-agent").unwrap(), "User-Agent");
300 assert_eq!(option_to_field_name("compression").unwrap(), "Compression");
301 }
302
303 #[test]
304 fn test_option_to_field_name_unknown() {
305 let result = option_to_field_name("unknownoption");
306 assert!(result.is_err());
307 match result {
308 Err(ConversionError::UnknownOption(opt)) => {
309 assert_eq!(opt, "unknownoption");
310 }
311 _ => panic!("Expected UnknownOption error"),
312 }
313 }
314
315 #[test]
316 fn test_roundtrip_conversion() {
317 let v4_input = r#"version=4
318opts=compression=xz,component=foo https://example.com/files .*/(\d+)\.tar\.gz
319"#;
320
321 let v4_file: WatchFile = v4_input.parse().unwrap();
322 let v5_file = convert_to_v5(&v4_file).unwrap();
323
324 let v5_str = ToString::to_string(&v5_file);
326 let v5_reparsed: WatchFileV5 = v5_str.parse().unwrap();
327
328 let entries: Vec<_> = v5_reparsed.entries().collect();
329 assert_eq!(entries.len(), 1);
330 assert_eq!(entries[0].component(), Some("foo".to_string()));
331 }
332}