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),
317 Line::Entry(entry) => write!(f, "{}", entry),
318 }
319 }
320}
321
322#[derive(Debug, PartialEq, Eq, Clone)]
323pub struct Maintscript {
325 lines: Vec<Line>,
326}
327
328impl Default for Maintscript {
329 fn default() -> Self {
330 Self::new()
331 }
332}
333
334impl Maintscript {
335 pub fn new() -> Self {
337 Maintscript { lines: Vec::new() }
338 }
339
340 pub fn is_empty(&self) -> bool {
342 self.lines.is_empty()
343 }
344
345 pub fn entries(&self) -> Vec<&Entry> {
347 self.lines
348 .iter()
349 .filter_map(|l| match l {
350 Line::Entry(e) => Some(e),
351 _ => None,
352 })
353 .collect()
354 }
355
356 pub fn remove(&mut self, index: usize) {
360 let mut comments: Vec<usize> = vec![];
361 let mut entries_seen = 0usize;
362 for (i, line) in self.lines.iter().enumerate() {
363 match line {
364 Line::Comment(_) => comments.push(i),
365 Line::Entry(_) => {
366 if entries_seen == index {
367 for c in comments.iter().rev() {
370 self.lines.remove(*c);
371 }
372 self.lines.remove(i - comments.len());
373 return;
374 }
375 entries_seen += 1;
376 comments.clear();
377 }
378 }
379 }
380 }
381}
382
383impl std::fmt::Display for Maintscript {
384 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
385 write!(
386 f,
387 "{}",
388 self.lines
389 .iter()
390 .map(|e| e.to_string())
391 .collect::<Vec<String>>()
392 .join("\n")
393 )
394 }
395}
396
397impl std::str::FromStr for Maintscript {
398 type Err = ParseError;
399
400 fn from_str(s: &str) -> Result<Self, Self::Err> {
401 let lines = s
402 .lines()
403 .map(|l| {
404 if l.starts_with('#') || l.trim().is_empty() {
405 Ok(Line::Comment(l.to_string()))
406 } else {
407 Ok(Line::Entry(Entry::from_str(l)?))
408 }
409 })
410 .collect::<Result<Vec<Line>, Self::Err>>()?;
411 Ok(Maintscript { lines })
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 #[test]
418 fn test_maintscript() {
419 let maintscript = "supports preinst
420rm_conffile /etc/foo.conf 1.2.3-4
421mv_conffile /etc/foo.conf /etc/bar.conf 1.2.3-4
422symlink_to_dir /etc/foo /etc/bar 1.2.3-4
423dir_to_symlink /etc/foo /etc/bar 1.2.3-4";
424 let maintscript = maintscript.parse::<super::Maintscript>().unwrap();
425 assert_eq!(
426 maintscript.entries(),
427 vec![
428 &super::Entry::Supports("preinst".to_string()),
429 &super::Entry::RemoveConffile {
430 conffile: "/etc/foo.conf".to_string(),
431 prior_version: Some("1.2.3-4".parse().unwrap()),
432 package: None
433 },
434 &super::Entry::MoveConffile {
435 old_conffile: "/etc/foo.conf".to_string(),
436 new_conffile: "/etc/bar.conf".to_string(),
437 prior_version: Some("1.2.3-4".parse().unwrap()),
438 package: None
439 },
440 &super::Entry::SymlinkToDir {
441 pathname: "/etc/foo".to_string(),
442 old_target: "/etc/bar".to_string(),
443 prior_version: Some("1.2.3-4".parse().unwrap()),
444 package: None
445 },
446 &super::Entry::DirToSymlink {
447 pathname: "/etc/foo".to_string(),
448 new_target: "/etc/bar".to_string(),
449 prior_version: Some("1.2.3-4".parse().unwrap()),
450 package: None
451 },
452 ]
453 );
454 }
455
456 #[test]
457 fn test_round_trip_preserves_comments() {
458 let original = "# leading comment\nrm_conffile /etc/foo.conf 1.2.3-4\n# trailing comment";
459 let parsed = original.parse::<super::Maintscript>().unwrap();
460 assert_eq!(parsed.to_string(), original);
461 }
462
463 #[test]
464 fn test_round_trip_preserves_blank_lines() {
465 let original = "rm_conffile /etc/foo.conf 1.2.3-4\n\nrm_conffile /etc/bar.conf 1.2.3-4";
466 let parsed = original.parse::<super::Maintscript>().unwrap();
467 assert_eq!(parsed.to_string(), original);
468 }
469
470 #[test]
471 fn test_remove_drops_preceding_comments() {
472 let original = "# comment for foo\nrm_conffile /etc/foo.conf 1.2.3-4\nrm_conffile /etc/bar.conf 1.2.3-4";
473 let mut parsed = original.parse::<super::Maintscript>().unwrap();
474 parsed.remove(0);
475 assert_eq!(parsed.to_string(), "rm_conffile /etc/bar.conf 1.2.3-4");
476 }
477}