1#[derive(Debug)]
5pub enum ParseError {
6 #[cfg(feature = "linebased")]
8 LineBased(crate::linebased::ParseError),
9 #[cfg(feature = "deb822")]
11 Deb822(crate::deb822::ParseError),
12 UnknownVersion,
14 FeatureNotEnabled(String),
16}
17
18impl std::fmt::Display for ParseError {
19 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
20 match self {
21 #[cfg(feature = "linebased")]
22 ParseError::LineBased(e) => write!(f, "{}", e),
23 #[cfg(feature = "deb822")]
24 ParseError::Deb822(e) => write!(f, "{}", e),
25 ParseError::UnknownVersion => write!(f, "Could not detect watch file version"),
26 ParseError::FeatureNotEnabled(msg) => write!(f, "{}", msg),
27 }
28 }
29}
30
31impl std::error::Error for ParseError {}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum WatchFileVersion {
36 LineBased(u32),
38 Deb822,
40}
41
42pub fn detect_version(content: &str) -> Option<WatchFileVersion> {
63 let trimmed = content.trim_start();
64
65 if trimmed.starts_with("Version:") || trimmed.starts_with("version:") {
67 if let Some(first_line) = trimmed.lines().next() {
69 if let Some(colon_pos) = first_line.find(':') {
70 let version_str = first_line[colon_pos + 1..].trim();
71 if version_str == "5" {
72 return Some(WatchFileVersion::Deb822);
73 }
74 }
75 }
76 }
77
78 for line in trimmed.lines() {
81 let line = line.trim();
82
83 if line.starts_with('#') || line.is_empty() {
85 continue;
86 }
87
88 if line.starts_with("version=") || line.starts_with("version =") {
90 let version_part = if line.starts_with("version=") {
91 &line[8..]
92 } else {
93 &line[9..]
94 };
95
96 if let Ok(version) = version_part.trim().parse::<u32>() {
97 return Some(WatchFileVersion::LineBased(version));
98 }
99 }
100
101 break;
103 }
104
105 Some(WatchFileVersion::LineBased(crate::DEFAULT_VERSION))
107}
108
109#[derive(Debug)]
111pub enum ParsedWatchFile {
112 #[cfg(feature = "linebased")]
114 LineBased(crate::linebased::WatchFile),
115 #[cfg(feature = "deb822")]
117 Deb822(crate::deb822::WatchFile),
118}
119
120#[derive(Debug)]
122pub enum ParsedEntry {
123 #[cfg(feature = "linebased")]
125 LineBased(crate::linebased::Entry),
126 #[cfg(feature = "deb822")]
128 Deb822(crate::deb822::Entry),
129}
130
131impl ParsedWatchFile {
132 pub fn version(&self) -> u32 {
134 match self {
135 #[cfg(feature = "linebased")]
136 ParsedWatchFile::LineBased(wf) => wf.version(),
137 #[cfg(feature = "deb822")]
138 ParsedWatchFile::Deb822(wf) => wf.version(),
139 }
140 }
141
142 pub fn entries(&self) -> impl Iterator<Item = ParsedEntry> + '_ {
144 let entries: Vec<_> = match self {
146 #[cfg(feature = "linebased")]
147 ParsedWatchFile::LineBased(wf) => wf.entries().map(ParsedEntry::LineBased).collect(),
148 #[cfg(feature = "deb822")]
149 ParsedWatchFile::Deb822(wf) => wf.entries().map(ParsedEntry::Deb822).collect(),
150 };
151 entries.into_iter()
152 }
153}
154
155impl ParsedEntry {
156 pub fn url(&self) -> String {
158 match self {
159 #[cfg(feature = "linebased")]
160 ParsedEntry::LineBased(e) => e.url(),
161 #[cfg(feature = "deb822")]
162 ParsedEntry::Deb822(e) => e.source().unwrap_or_default(),
163 }
164 }
165
166 pub fn matching_pattern(&self) -> Option<String> {
168 match self {
169 #[cfg(feature = "linebased")]
170 ParsedEntry::LineBased(e) => e.matching_pattern(),
171 #[cfg(feature = "deb822")]
172 ParsedEntry::Deb822(e) => e.matching_pattern(),
173 }
174 }
175
176 pub fn get_option(&self, key: &str) -> Option<String> {
182 match self {
183 #[cfg(feature = "linebased")]
184 ParsedEntry::LineBased(e) => e.get_option(key),
185 #[cfg(feature = "deb822")]
186 ParsedEntry::Deb822(e) => {
187 e.get_field(key).or_else(|| {
189 let mut chars = key.chars();
190 if let Some(first) = chars.next() {
191 let capitalized = first.to_uppercase().chain(chars).collect::<String>();
192 e.get_field(&capitalized)
193 } else {
194 None
195 }
196 })
197 }
198 }
199 }
200
201 pub fn has_option(&self, key: &str) -> bool {
203 self.get_option(key).is_some()
204 }
205
206 pub fn script(&self) -> Option<String> {
208 self.get_option("script")
209 }
210
211 pub fn format_url(
213 &self,
214 package: impl FnOnce() -> String,
215 ) -> Result<url::Url, url::ParseError> {
216 crate::subst::subst(&self.url(), package).parse()
217 }
218
219 pub fn user_agent(&self) -> Option<String> {
221 self.get_option("user-agent")
222 }
223
224 pub fn pagemangle(&self) -> Option<String> {
226 self.get_option("pagemangle")
227 }
228
229 pub fn uversionmangle(&self) -> Option<String> {
231 self.get_option("uversionmangle")
232 }
233
234 pub fn downloadurlmangle(&self) -> Option<String> {
236 self.get_option("downloadurlmangle")
237 }
238
239 pub fn pgpsigurlmangle(&self) -> Option<String> {
241 self.get_option("pgpsigurlmangle")
242 }
243
244 pub fn filenamemangle(&self) -> Option<String> {
246 self.get_option("filenamemangle")
247 }
248
249 pub fn oversionmangle(&self) -> Option<String> {
251 self.get_option("oversionmangle")
252 }
253
254 pub fn searchmode(&self) -> crate::types::SearchMode {
256 self.get_option("searchmode")
257 .and_then(|s| s.parse().ok())
258 .unwrap_or_default()
259 }
260}
261
262impl std::fmt::Display for ParsedWatchFile {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 match self {
265 #[cfg(feature = "linebased")]
266 ParsedWatchFile::LineBased(wf) => write!(f, "{}", wf),
267 #[cfg(feature = "deb822")]
268 ParsedWatchFile::Deb822(wf) => write!(f, "{}", wf),
269 }
270 }
271}
272
273pub fn parse(content: &str) -> Result<ParsedWatchFile, ParseError> {
292 let version = detect_version(content).ok_or(ParseError::UnknownVersion)?;
293
294 match version {
295 #[cfg(feature = "linebased")]
296 WatchFileVersion::LineBased(_v) => {
297 let wf: crate::linebased::WatchFile = content
298 .parse()
299 .map_err(ParseError::LineBased)?;
300 Ok(ParsedWatchFile::LineBased(wf))
301 }
302 #[cfg(not(feature = "linebased"))]
303 WatchFileVersion::LineBased(_v) => {
304 Err(ParseError::FeatureNotEnabled("linebased feature required for v1-4 formats".to_string()))
305 }
306 #[cfg(feature = "deb822")]
307 WatchFileVersion::Deb822 => {
308 let wf: crate::deb822::WatchFile = content
309 .parse()
310 .map_err(ParseError::Deb822)?;
311 Ok(ParsedWatchFile::Deb822(wf))
312 }
313 #[cfg(not(feature = "deb822"))]
314 WatchFileVersion::Deb822 => {
315 Err(ParseError::FeatureNotEnabled("deb822 feature required for v5 format".to_string()))
316 }
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_detect_version_v1_default() {
326 let content = "https://example.com/ .*.tar.gz";
327 assert_eq!(
328 detect_version(content),
329 Some(WatchFileVersion::LineBased(1))
330 );
331 }
332
333 #[test]
334 fn test_detect_version_v4() {
335 let content = "version=4\nhttps://example.com/ .*.tar.gz";
336 assert_eq!(
337 detect_version(content),
338 Some(WatchFileVersion::LineBased(4))
339 );
340 }
341
342 #[test]
343 fn test_detect_version_v4_with_spaces() {
344 let content = "version = 4\nhttps://example.com/ .*.tar.gz";
345 assert_eq!(
346 detect_version(content),
347 Some(WatchFileVersion::LineBased(4))
348 );
349 }
350
351 #[test]
352 fn test_detect_version_v5() {
353 let content = "Version: 5\n\nSource: https://example.com/";
354 assert_eq!(detect_version(content), Some(WatchFileVersion::Deb822));
355 }
356
357 #[test]
358 fn test_detect_version_v5_lowercase() {
359 let content = "version: 5\n\nSource: https://example.com/";
360 assert_eq!(detect_version(content), Some(WatchFileVersion::Deb822));
361 }
362
363 #[test]
364 fn test_detect_version_with_leading_comments() {
365 let content = "# This is a comment\nversion=4\nhttps://example.com/ .*.tar.gz";
366 assert_eq!(
367 detect_version(content),
368 Some(WatchFileVersion::LineBased(4))
369 );
370 }
371
372 #[test]
373 fn test_detect_version_with_leading_whitespace() {
374 let content = " \n version=3\nhttps://example.com/ .*.tar.gz";
375 assert_eq!(
376 detect_version(content),
377 Some(WatchFileVersion::LineBased(3))
378 );
379 }
380
381 #[test]
382 fn test_detect_version_v2() {
383 let content = "version=2\nhttps://example.com/ .*.tar.gz";
384 assert_eq!(
385 detect_version(content),
386 Some(WatchFileVersion::LineBased(2))
387 );
388 }
389
390 #[cfg(feature = "linebased")]
391 #[test]
392 fn test_parse_linebased() {
393 let content = "version=4\nhttps://example.com/ .*.tar.gz";
394 let parsed = parse(content).unwrap();
395 assert_eq!(parsed.version(), 4);
396 }
397
398 #[cfg(feature = "deb822")]
399 #[test]
400 fn test_parse_deb822() {
401 let content = "Version: 5\n\nSource: https://example.com/\nMatching-Pattern: .*.tar.gz";
402 let parsed = parse(content).unwrap();
403 assert_eq!(parsed.version(), 5);
404 }
405
406 #[cfg(all(feature = "linebased", feature = "deb822"))]
407 #[test]
408 fn test_parse_both_formats() {
409 let v4_content = "version=4\nhttps://example.com/ .*.tar.gz";
411 let v4_parsed = parse(v4_content).unwrap();
412 assert_eq!(v4_parsed.version(), 4);
413
414 let v5_content = "Version: 5\n\nSource: https://example.com/\nMatching-Pattern: .*.tar.gz";
416 let v5_parsed = parse(v5_content).unwrap();
417 assert_eq!(v5_parsed.version(), 5);
418 }
419
420 #[cfg(feature = "linebased")]
421 #[test]
422 fn test_parse_roundtrip() {
423 let content = "version=4\n# Comment\nhttps://example.com/ .*.tar.gz";
424 let parsed = parse(content).unwrap();
425 let output = parsed.to_string();
426
427 let reparsed = parse(&output).unwrap();
429 assert_eq!(reparsed.version(), 4);
430 }
431}