debian_analyzer/
maintscripts.rs

1//! Maintscript file parsing and generation
2use debversion::Version;
3
4#[derive(Debug, PartialEq, Eq)]
5/// An error that occurred while parsing a maintscript file
6pub enum ParseError {
7    /// An unknown maintscript command
8    UnknownCommand(String),
9    /// A maintscript command is missing an argument
10    MissingArgument(String),
11    /// An invalid version was encountered
12    InvalidVersion(debversion::ParseError),
13}
14
15impl std::fmt::Display for ParseError {
16    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
17        match self {
18            ParseError::UnknownCommand(command) => {
19                write!(f, "Unknown maintscript command: {}", command)
20            }
21            ParseError::MissingArgument(command) => {
22                write!(f, "Missing argument for maintscript command: {}", command)
23            }
24            ParseError::InvalidVersion(e) => write!(f, "Invalid version: {}", e),
25        }
26    }
27}
28
29impl std::error::Error for ParseError {}
30
31impl From<debversion::ParseError> for ParseError {
32    fn from(e: debversion::ParseError) -> Self {
33        ParseError::InvalidVersion(e)
34    }
35}
36
37#[derive(Debug, PartialEq, Eq, Clone)]
38/// An entry in a maintscript file
39pub enum Entry {
40    /// A command that is supported by the maintscript
41    Supports(String),
42    /// Remove a conffile
43    RemoveConffile {
44        /// The pathname of the conffile
45        conffile: String,
46        /// The version of the package that is being upgraded
47        prior_version: Option<Version>,
48        /// The name of the package that is being upgraded
49        package: Option<String>,
50    },
51    /// Move a conffile
52    MoveConffile {
53        /// The old pathname of the conffile
54        old_conffile: String,
55        /// The new pathname of the conffile
56        new_conffile: String,
57        /// The version of the package that is being upgraded
58        prior_version: Option<Version>,
59        /// The name of the package that is being upgraded
60        package: Option<String>,
61    },
62    /// Convert a symlink to a directory
63    SymlinkToDir {
64        /// The pathname of the symlink
65        pathname: String,
66        /// The old target of the symlink
67        old_target: String,
68        /// The version of the package that is being upgraded
69        prior_version: Option<Version>,
70        /// The name of the package that is being upgraded
71        package: Option<String>,
72    },
73    /// Convert a directory to a symlink
74    DirToSymlink {
75        /// The pathname of the directory
76        pathname: String,
77        /// The new target of the symlink
78        new_target: String,
79        /// The version of the package that is being upgraded
80        prior_version: Option<Version>,
81        /// The name of the package that is being upgraded
82        package: Option<String>,
83    },
84}
85
86impl Entry {
87    /// Get the arguments of the entry
88    fn args(&self) -> Vec<String> {
89        match self {
90            Entry::Supports(command) => vec!["supports".to_string(), command.to_string()],
91            Entry::RemoveConffile {
92                conffile,
93                prior_version,
94                package,
95            } => {
96                let mut ret = vec!["rm_conffile".to_string(), conffile.to_string()];
97                if let Some(prior_version) = prior_version.as_ref() {
98                    ret.push(prior_version.to_string());
99                    if let Some(package) = package.as_ref() {
100                        ret.push(package.to_string());
101                    }
102                }
103                ret
104            }
105            Entry::MoveConffile {
106                old_conffile,
107                new_conffile,
108                prior_version,
109                package,
110            } => {
111                let mut ret = vec![
112                    "mv_conffile".to_string(),
113                    old_conffile.to_string(),
114                    new_conffile.to_string(),
115                ];
116                if let Some(prior_version) = prior_version.as_ref() {
117                    ret.push(prior_version.to_string());
118                    if let Some(package) = package.as_ref() {
119                        ret.push(package.to_string());
120                    }
121                }
122                ret
123            }
124            Entry::SymlinkToDir {
125                pathname,
126                old_target,
127                prior_version,
128                package,
129            } => {
130                let mut ret = vec![
131                    "symlink_to_dir".to_string(),
132                    pathname.to_string(),
133                    old_target.to_string(),
134                ];
135                if let Some(prior_version) = prior_version.as_ref() {
136                    ret.push(prior_version.to_string());
137                    if let Some(package) = package.as_ref() {
138                        ret.push(package.to_string());
139                    }
140                }
141                ret
142            }
143            Entry::DirToSymlink {
144                pathname,
145                new_target,
146                prior_version,
147                package,
148            } => {
149                let mut ret = vec![
150                    "dir_to_symlink".to_string(),
151                    pathname.to_string(),
152                    new_target.to_string(),
153                ];
154                if let Some(prior_version) = prior_version.as_ref() {
155                    ret.push(prior_version.to_string());
156                    if let Some(package) = package.as_ref() {
157                        ret.push(package.to_string());
158                    }
159                }
160                ret
161            }
162        }
163    }
164
165    /// Get the name of the package that is being upgraded
166    pub fn package(&self) -> Option<&String> {
167        match self {
168            Entry::RemoveConffile { package, .. } => package.as_ref(),
169            Entry::MoveConffile { package, .. } => package.as_ref(),
170            Entry::SymlinkToDir { package, .. } => package.as_ref(),
171            Entry::DirToSymlink { package, .. } => package.as_ref(),
172            _ => None,
173        }
174    }
175
176    /// Get the version of the package that is being upgraded
177    pub fn prior_version(&self) -> Option<&Version> {
178        match self {
179            Entry::RemoveConffile { prior_version, .. } => prior_version.as_ref(),
180            Entry::MoveConffile { prior_version, .. } => prior_version.as_ref(),
181            Entry::SymlinkToDir { prior_version, .. } => prior_version.as_ref(),
182            Entry::DirToSymlink { prior_version, .. } => prior_version.as_ref(),
183            _ => None,
184        }
185    }
186}
187
188impl std::fmt::Display for Entry {
189    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
190        write!(f, "{}", self.args().join(" "))
191    }
192}
193
194impl std::str::FromStr for Entry {
195    type Err = ParseError;
196
197    fn from_str(s: &str) -> Result<Self, Self::Err> {
198        let args: Vec<&str> = s.split_whitespace().collect();
199        match args[0] {
200            "supports" => {
201                if args.len() != 2 {
202                    return Err(ParseError::MissingArgument("supports".to_string()));
203                }
204                Ok(Entry::Supports(args[1].to_string()))
205            }
206            "rm_conffile" => {
207                if args.len() < 2 {
208                    return Err(ParseError::MissingArgument("rm_conffile".to_string()));
209                }
210                let conffile = args[1].to_string();
211                let prior_version = if args.len() > 2 {
212                    Some(args[2].parse()?)
213                } else {
214                    None
215                };
216                let package = if args.len() > 3 {
217                    Some(args[3].to_string())
218                } else {
219                    None
220                };
221                Ok(Entry::RemoveConffile {
222                    conffile,
223                    prior_version,
224                    package,
225                })
226            }
227            "mv_conffile" => {
228                if args.len() < 3 {
229                    return Err(ParseError::MissingArgument("mv_conffile".to_string()));
230                }
231                let old_conffile = args[1].to_string();
232                let new_conffile = args[2].to_string();
233                let prior_version = if args.len() > 3 {
234                    Some(args[3].parse()?)
235                } else {
236                    None
237                };
238                let package = if args.len() > 4 {
239                    Some(args[4].to_string())
240                } else {
241                    None
242                };
243                Ok(Entry::MoveConffile {
244                    old_conffile,
245                    new_conffile,
246                    prior_version,
247                    package,
248                })
249            }
250            "symlink_to_dir" => {
251                if args.len() < 3 {
252                    return Err(ParseError::MissingArgument("symlink_to_dir".to_string()));
253                }
254                let pathname = args[1].to_string();
255                let old_target = args[2].to_string();
256                let prior_version = if args.len() > 3 {
257                    Some(args[3].parse()?)
258                } else {
259                    None
260                };
261                let package = if args.len() > 4 {
262                    Some(args[4].to_string())
263                } else {
264                    None
265                };
266                Ok(Entry::SymlinkToDir {
267                    pathname,
268                    old_target,
269                    prior_version,
270                    package,
271                })
272            }
273            "dir_to_symlink" => {
274                if args.len() < 3 {
275                    return Err(ParseError::MissingArgument("dir_to_symlink".to_string()));
276                }
277                let pathname = args[1].to_string();
278                let new_target = args[2].to_string();
279                let prior_version = if args.len() > 3 {
280                    Some(args[3].parse()?)
281                } else {
282                    None
283                };
284                let package = if args.len() > 4 {
285                    Some(args[4].to_string())
286                } else {
287                    None
288                };
289                Ok(Entry::DirToSymlink {
290                    pathname,
291                    new_target,
292                    prior_version,
293                    package,
294                })
295            }
296            n => Err(ParseError::UnknownCommand(n.to_string())),
297        }
298    }
299}
300
301#[derive(Debug, PartialEq, Eq, Clone)]
302/// A line in a maintscript file
303enum Line {
304    /// A comment
305    Comment(String),
306    /// An entry
307    Entry(Entry),
308}
309
310impl std::fmt::Display for Line {
311    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
312        match self {
313            Line::Comment(comment) => write!(f, "# {}", comment),
314            Line::Entry(entry) => write!(f, "{}", entry),
315        }
316    }
317}
318
319#[derive(Debug, PartialEq, Eq, Clone)]
320/// A maintscript file
321pub struct Maintscript {
322    lines: Vec<Line>,
323}
324
325impl Default for Maintscript {
326    fn default() -> Self {
327        Self::new()
328    }
329}
330
331impl Maintscript {
332    /// Create a new maintscript file
333    pub fn new() -> Self {
334        Maintscript { lines: Vec::new() }
335    }
336
337    /// Check if the maintscript file is empty
338    pub fn is_empty(&self) -> bool {
339        self.lines.is_empty()
340    }
341
342    /// Iterate over the entries in the maintscript file
343    pub fn entries(&self) -> Vec<&Entry> {
344        self.lines
345            .iter()
346            .filter_map(|l| match l {
347                Line::Entry(e) => Some(e),
348                _ => None,
349            })
350            .collect()
351    }
352
353    /// Remove an entry from the maintscript file
354    pub fn remove(&mut self, index: usize) {
355        // Also remove preceding comments
356        let mut comments = vec![];
357        for (i, line) in self.lines.iter().enumerate() {
358            match line {
359                Line::Comment(_) => comments.push(i),
360                Line::Entry(_) => {
361                    if i == index {
362                        for i in comments.iter().rev() {
363                            self.lines.remove(*i);
364                        }
365                        self.lines.remove(index - comments.len());
366                        return;
367                    }
368                    comments.clear();
369                }
370            }
371        }
372    }
373}
374
375impl std::fmt::Display for Maintscript {
376    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
377        write!(
378            f,
379            "{}",
380            self.lines
381                .iter()
382                .map(|e| e.to_string())
383                .collect::<Vec<String>>()
384                .join("\n")
385        )
386    }
387}
388
389impl std::str::FromStr for Maintscript {
390    type Err = ParseError;
391
392    fn from_str(s: &str) -> Result<Self, Self::Err> {
393        let lines = s
394            .lines()
395            .map(|l| {
396                if l.starts_with('#') || l.trim().is_empty() {
397                    Ok(Line::Comment(l.to_string()))
398                } else {
399                    Ok(Line::Entry(Entry::from_str(l)?))
400                }
401            })
402            .collect::<Result<Vec<Line>, Self::Err>>()?;
403        Ok(Maintscript { lines })
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    #[test]
410    fn test_maintscript() {
411        let maintscript = "supports preinst
412rm_conffile /etc/foo.conf 1.2.3-4
413mv_conffile /etc/foo.conf /etc/bar.conf 1.2.3-4
414symlink_to_dir /etc/foo /etc/bar 1.2.3-4
415dir_to_symlink /etc/foo /etc/bar 1.2.3-4";
416        let maintscript = maintscript.parse::<super::Maintscript>().unwrap();
417        assert_eq!(
418            maintscript.entries(),
419            vec![
420                &super::Entry::Supports("preinst".to_string()),
421                &super::Entry::RemoveConffile {
422                    conffile: "/etc/foo.conf".to_string(),
423                    prior_version: Some("1.2.3-4".parse().unwrap()),
424                    package: None
425                },
426                &super::Entry::MoveConffile {
427                    old_conffile: "/etc/foo.conf".to_string(),
428                    new_conffile: "/etc/bar.conf".to_string(),
429                    prior_version: Some("1.2.3-4".parse().unwrap()),
430                    package: None
431                },
432                &super::Entry::SymlinkToDir {
433                    pathname: "/etc/foo".to_string(),
434                    old_target: "/etc/bar".to_string(),
435                    prior_version: Some("1.2.3-4".parse().unwrap()),
436                    package: None
437                },
438                &super::Entry::DirToSymlink {
439                    pathname: "/etc/foo".to_string(),
440                    new_target: "/etc/bar".to_string(),
441                    prior_version: Some("1.2.3-4".parse().unwrap()),
442                    package: None
443                },
444            ]
445        );
446    }
447}