1use debversion::Version;
3
4#[derive(Debug, PartialEq, Eq)]
5pub enum ParseError {
7 UnknownCommand(String),
9 MissingArgument(String),
11 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)]
38pub enum Entry {
40 Supports(String),
42 RemoveConffile {
44 conffile: String,
46 prior_version: Option<Version>,
48 package: Option<String>,
50 },
51 MoveConffile {
53 old_conffile: String,
55 new_conffile: String,
57 prior_version: Option<Version>,
59 package: Option<String>,
61 },
62 SymlinkToDir {
64 pathname: String,
66 old_target: String,
68 prior_version: Option<Version>,
70 package: Option<String>,
72 },
73 DirToSymlink {
75 pathname: String,
77 new_target: String,
79 prior_version: Option<Version>,
81 package: Option<String>,
83 },
84}
85
86impl Entry {
87 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 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 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)]
302enum Line {
304 Comment(String),
306 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)]
320pub 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 pub fn new() -> Self {
334 Maintscript { lines: Vec::new() }
335 }
336
337 pub fn is_empty(&self) -> bool {
339 self.lines.is_empty()
340 }
341
342 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 pub fn remove(&mut self, index: usize) {
355 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}