oma_apt_sources_lists/
sources_list.rs

1use self::source_deb822::SourceListDeb822;
2
3use super::*;
4use std::collections::HashSet;
5use std::fmt::{self, Display, Formatter};
6use std::fs::{self, File};
7use std::io::{self, Write};
8use std::ops::{Deref, DerefMut};
9use std::path::{Path, PathBuf};
10use std::str::FromStr;
11
12#[derive(Clone, Debug)]
13pub struct SourcesList {
14    pub path: PathBuf,
15    pub entries: SourceListType,
16}
17
18#[derive(PartialEq, Clone, Debug)]
19pub enum SourceListType {
20    SourceLine(SourceListLineStyle),
21    Deb822(SourceListDeb822),
22}
23
24#[derive(PartialEq, Clone, Debug)]
25pub struct SourceListLineStyle(pub Vec<SourceLine>);
26
27impl FromStr for SourceListLineStyle {
28    type Err = SourcesListError;
29
30    fn from_str(s: &str) -> Result<Self, Self::Err> {
31        let mut entries = vec![];
32        for (line_num, line) in s.lines().enumerate() {
33            let entry = line
34                .parse::<SourceLine>()
35                .map_err(|why| SourcesListError::BadLine {
36                    line: line_num,
37                    why,
38                })?;
39
40            entries.push(entry);
41        }
42
43        Ok(SourceListLineStyle(entries))
44    }
45}
46
47impl SourcesList {
48    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, SourcesListError> {
49        let path = path.as_ref();
50        let data = fs::read_to_string(path).map_err(|why| SourcesListError::SourcesListOpen {
51            path: path.to_path_buf(),
52            why,
53        })?;
54
55        let mut sources_file = match path.extension() {
56            Some(x) if x == "sources" => SourcesList {
57                path: path.to_path_buf(),
58                entries: SourceListType::Deb822(SourceListDeb822::from_str(&data).map_err(
59                    |e| SourcesListError::Deb822 {
60                        path: path.to_path_buf(),
61                        why: e,
62                    },
63                )?),
64            },
65            Some(x) if x == "list" => get_line_style_sources_list(&data)?,
66            _ => {
67                return Err(SourcesListError::UnknownFile {
68                    path: path.to_path_buf(),
69                })
70            }
71        };
72
73        sources_file.path = path.to_path_buf();
74
75        Ok(sources_file)
76    }
77
78    pub fn contains_entry(&self, entry: &str) -> Option<usize> {
79        let elem = &self.entries;
80        match elem {
81            SourceListType::SourceLine(lines) => lines.0.iter().position(|e| {
82                if let SourceLine::Entry(e) = e {
83                    e.url == entry
84                } else {
85                    false
86                }
87            }),
88            SourceListType::Deb822(e) => e.entries.iter().position(|x| x.url == entry),
89        }
90    }
91
92    pub fn get_entries_mut<'a>(
93        &'a mut self,
94        entry: &'a str,
95    ) -> Box<dyn Iterator<Item = &'a mut SourceEntry> + 'a> {
96        match self.entries {
97            SourceListType::SourceLine(ref mut line) => {
98                Box::new(line.0.iter_mut().filter_map(move |line| {
99                    if let SourceLine::Entry(ref mut e) = line {
100                        if entry == e.url {
101                            return Some(e);
102                        }
103                    }
104
105                    None
106                }))
107            }
108            SourceListType::Deb822(ref mut e) => {
109                Box::new(e.entries.iter_mut().filter_map(move |e| {
110                    if entry == e.url {
111                        return Some(e);
112                    }
113
114                    None
115                }))
116            }
117        }
118    }
119
120    pub fn is_active(&self) -> bool {
121        match &self.entries {
122            SourceListType::SourceLine(line) => line
123                .0
124                .iter()
125                .any(|line| matches!(line, SourceLine::Entry(_))),
126            SourceListType::Deb822(e) => !e.entries.is_empty(),
127        }
128    }
129
130    pub fn write_sync(&mut self) -> io::Result<()> {
131        fs::OpenOptions::new()
132            .truncate(true)
133            .write(true)
134            .open(&self.path)
135            .and_then(|mut file| writeln!(&mut file, "{}", self))
136    }
137
138    pub fn reload(&mut self) -> Result<(), SourcesListError> {
139        *self = Self::new(&self.path)?;
140        Ok(())
141    }
142}
143
144fn get_line_style_sources_list(data: &str) -> Result<SourcesList, SourcesListError> {
145    let mut entries = vec![];
146    for (line_num, line) in data.lines().enumerate() {
147        let entry = line
148            .parse::<SourceLine>()
149            .map_err(|why| SourcesListError::BadLine {
150                line: line_num,
151                why,
152            })?;
153
154        entries.push(entry);
155    }
156
157    Ok(SourcesList {
158        path: PathBuf::from(""),
159        entries: SourceListType::SourceLine(SourceListLineStyle(entries)),
160    })
161}
162
163impl Display for SourcesList {
164    fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
165        match &self.entries {
166            SourceListType::SourceLine(lines) => {
167                for line in &lines.0 {
168                    writeln!(fmt, "{}", line)?;
169                }
170            }
171            SourceListType::Deb822(e) => {
172                for entry in &e.entries {
173                    writeln!(fmt, "{}", entry)?;
174                }
175            }
176        }
177
178        Ok(())
179    }
180}
181
182#[derive(Clone, Debug)]
183/// Stores all apt source information fetched from the system.
184pub struct SourcesLists {
185    pub(crate) files: Vec<SourcesList>,
186    pub(crate) modified: Vec<u16>,
187}
188
189impl Deref for SourcesLists {
190    type Target = Vec<SourcesList>;
191
192    fn deref(&self) -> &Self::Target {
193        &self.files
194    }
195}
196
197impl DerefMut for SourcesLists {
198    fn deref_mut(&mut self) -> &mut Self::Target {
199        &mut self.files
200    }
201}
202
203impl SourcesLists {
204    /// Scans every file in **/etc/apt/sources.list.d**, including **/etc/apt/sources.list**.
205    ///
206    /// Note that this will parse every source list into memory before returning.
207    pub fn scan() -> Result<Self, SourcesListError> {
208        scan_inner("/")
209    }
210
211    /// Scans every file in **/etc/apt/sources.list.d**, including **/etc/apt/sources.list**. (from root argument)
212    ///
213    /// Note that this will parse every source list into memory before returning.
214    pub fn scan_from_root<P: AsRef<Path>>(root: P) -> Result<Self, SourcesListError> {
215        scan_inner(root)
216    }
217
218    /// When given a list of paths to source lists, this will attempt to parse them.
219    pub fn new_from_paths<P: AsRef<Path>, I: Iterator<Item = P>>(
220        paths: I,
221    ) -> Result<Self, SourcesListError> {
222        let files = paths
223            .map(SourcesList::new)
224            .collect::<Result<Vec<SourcesList>, SourcesListError>>()?;
225
226        Ok(SourcesLists {
227            modified: Vec::with_capacity(files.len()),
228            files,
229        })
230    }
231
232    /// Specify to enable or disable a repo. `true` is returned if the repo was found.
233    pub fn repo_modify(&mut self, repo: &str, enabled: bool) -> bool {
234        let &mut Self {
235            ref mut modified,
236            ref mut files,
237        } = self;
238
239        let iterator = files
240            .iter_mut()
241            .enumerate()
242            .flat_map(|(pos, list)| list.get_entries_mut(repo).map(move |e| (pos, e)));
243
244        let mut found = false;
245        for (pos, entry) in iterator {
246            add_modified(modified, pos as u16);
247            entry.enabled = enabled;
248            found = true;
249        }
250
251        found
252    }
253
254    /// Constructs an iterator of enabled source entries from a sources list.
255    pub fn entries(&self) -> impl Iterator<Item = &SourceEntry> {
256        self.iter()
257            .flat_map(|list| -> Box<dyn Iterator<Item = &SourceEntry>> {
258                match &list.entries {
259                    SourceListType::SourceLine(lines) => Box::new(lines.0.iter().filter_map(|x| {
260                        if let SourceLine::Entry(entry) = x {
261                            Some(entry)
262                        } else {
263                            None
264                        }
265                    })),
266                    SourceListType::Deb822(e) => Box::new(e.entries.iter()),
267                }
268            })
269    }
270
271    /// A callback-based iterator that tracks which files have been modified.
272    pub fn entries_mut<F: FnMut(&mut SourceEntry) -> bool>(&mut self, mut func: F) {
273        let &mut Self {
274            ref mut files,
275            ref mut modified,
276        } = self;
277        for (pos, list) in files.iter_mut().enumerate() {
278            match list.entries {
279                SourceListType::SourceLine(ref mut lines) => {
280                    for entry in &mut lines.0 {
281                        if let SourceLine::Entry(entry) = entry {
282                            if func(entry) {
283                                add_modified(modified, pos as u16)
284                            }
285                        }
286                    }
287                }
288                SourceListType::Deb822(ref mut e) => {
289                    for entry in &mut e.entries {
290                        if func(entry) {
291                            add_modified(modified, pos as u16)
292                        }
293                    }
294                }
295            }
296        }
297    }
298
299    /// Insert a source entry to the lists.
300    ///
301    /// If the entry already exists, it will be modified.
302    /// Otherwise, the entry will be added to the preferred list.
303    /// If the preferred list does not exist, it will be created.
304    pub fn insert_entry<P: AsRef<Path>>(
305        &mut self,
306        path: P,
307        entry: SourceEntry,
308    ) -> SourceResult<()> {
309        let path = path.as_ref();
310        let &mut Self {
311            ref mut modified,
312            ref mut files,
313        } = self;
314
315        for (id, list) in files.iter_mut().enumerate() {
316            if list.path == path {
317                match list.contains_entry(&entry.url) {
318                    Some(pos) => match list.entries {
319                        SourceListType::SourceLine(ref mut lines) => {
320                            lines.0[pos] = SourceLine::Entry(entry)
321                        }
322                        SourceListType::Deb822(ref mut e) => {
323                            e.entries[pos] = entry;
324                        }
325                    },
326                    None => match list.entries {
327                        SourceListType::SourceLine(ref mut lines) => {
328                            lines.0.push(SourceLine::Entry(entry))
329                        }
330                        SourceListType::Deb822(ref mut e) => {
331                            e.entries.push(entry);
332                        }
333                    },
334                }
335
336                add_modified(modified, id as u16);
337                return Ok(());
338            }
339        }
340
341        files.push(SourcesList {
342            path: path.to_path_buf(),
343            entries: SourceListType::SourceLine(SourceListLineStyle(vec![SourceLine::Entry(
344                entry,
345            )])),
346        });
347
348        Ok(())
349    }
350
351    /// Remove the source entry from each file in the sources lists.
352    pub fn remove_entry(&mut self, repo: &str) {
353        let &mut Self {
354            ref mut modified,
355            ref mut files,
356        } = self;
357        for (id, list) in files.iter_mut().enumerate() {
358            if let Some(line) = list.contains_entry(repo) {
359                match list.entries {
360                    SourceListType::SourceLine(ref mut lines) => {
361                        lines.0.remove(line);
362                    }
363                    SourceListType::Deb822(ref mut e) => {
364                        e.entries.remove(line);
365                    }
366                }
367                add_modified(modified, id as u16);
368            }
369        }
370    }
371
372    /// Modify all sources with the `from_suite` to point to the `to_suite`.
373    ///
374    /// Changes are only applied in-memory. Use `SourcesLists::wirte_sync` to write
375    /// all changes to the disk.
376    pub fn dist_replace(&mut self, from_suite: &str, to_suite: &str) {
377        let &mut Self {
378            ref mut modified,
379            ref mut files,
380        } = self;
381        for (id, file) in files.iter_mut().enumerate() {
382            let mut changed = false;
383            match file.entries {
384                SourceListType::SourceLine(ref mut lines) => {
385                    for line in &mut lines.0 {
386                        if let SourceLine::Entry(ref mut entry) = line {
387                            if entry.suite.starts_with(from_suite) {
388                                entry.suite = entry.suite.replace(from_suite, to_suite);
389                                changed = true;
390                            }
391                        }
392                    }
393                }
394                SourceListType::Deb822(ref mut e) => {
395                    for entry in &mut e.entries {
396                        if entry.suite.starts_with(from_suite) {
397                            entry.suite = entry.suite.replace(from_suite, to_suite);
398                            changed = true;
399                        }
400                    }
401                }
402            }
403
404            if changed {
405                add_modified(modified, id as u16);
406            }
407        }
408    }
409
410    /// Upgrade entries so that they point to a new release.
411    ///
412    /// Files are copied to "$path.save" before being overwritten. On failure, these backup files
413    /// will be used to restore the original list.
414    pub fn dist_upgrade(
415        &mut self,
416        retain: &HashSet<Box<str>>,
417        from_suite: &str,
418        to_suite: &str,
419    ) -> io::Result<()> {
420        fn newfile(modified: &mut Vec<PathBuf>, path: &Path) -> io::Result<File> {
421            let backup_path = path
422                .file_name()
423                .map(|str| {
424                    let mut string = str.to_os_string();
425                    string.push(".save");
426
427                    let mut backup = path.to_path_buf();
428                    backup.set_file_name(&string);
429                    backup
430                })
431                .ok_or_else(|| {
432                    io::Error::new(
433                        io::ErrorKind::NotFound,
434                        format!("filename not found for apt source at '{}'", path.display()),
435                    )
436                })?;
437
438            fs::copy(path, &backup_path)?;
439            modified.push(backup_path);
440            fs::OpenOptions::new().truncate(true).write(true).open(path)
441        }
442
443        fn apply(
444            sources: &mut SourcesLists,
445            modified: &mut Vec<PathBuf>,
446            retain: &HashSet<Box<str>>,
447            from_suite: &str,
448            to_suite: &str,
449        ) -> io::Result<()> {
450            for list in sources.iter_mut() {
451                let mut current_file = newfile(modified, &list.path)?;
452
453                match list.entries {
454                    SourceListType::SourceLine(ref mut lines) => {
455                        for line in &mut lines.0 {
456                            if let SourceLine::Entry(entry) = line {
457                                if !retain.contains(entry.url.as_str())
458                                    && entry.url.starts_with("http")
459                                    && entry.suite.starts_with(from_suite)
460                                {
461                                    entry.suite = entry.suite.replace(from_suite, to_suite);
462                                }
463                            }
464
465                            writeln!(&mut current_file, "{}", line)?
466                        }
467                    }
468                    SourceListType::Deb822(ref mut e) => writeln!(&mut current_file, "{}", e)?,
469                }
470
471                current_file.flush()?;
472            }
473
474            Ok(())
475        }
476
477        let mut modified = Vec::new();
478        apply(self, &mut modified, retain, from_suite, to_suite).inspect_err(|_| {
479            // TODO: Revert the ipathsn-memory changes that were made when being applied.
480            // revert(self, &modified);
481
482            for (original, backup) in self.iter().zip(modified.iter()) {
483                if let Err(why) = fs::copy(backup, &original.path) {
484                    eprintln!("failed to restore backup of {:?}: {}", backup, why);
485                }
486            }
487        })
488    }
489
490    /// Retrieve an iterator of upgradeable paths.
491    ///
492    /// All source entries that have the `from_suite` will have new URLs constructed with the
493    /// `to_suite`.
494    pub fn dist_upgrade_paths<'a>(
495        &'a self,
496        from_suite: &'a str,
497        to_suite: &'a str,
498    ) -> impl Iterator<Item = String> + 'a {
499        self.entries().filter_map(move |entry| {
500            if entry.url.starts_with("http") && entry.suite.starts_with(from_suite) {
501                let entry = {
502                    let mut entry = entry.clone();
503                    entry.suite = entry.suite.replace(from_suite, to_suite);
504                    entry
505                };
506
507                let dist_path = entry.dist_path();
508                Some(dist_path)
509            } else {
510                None
511            }
512        })
513    }
514
515    /// Overwrite all files which were modified.
516    pub fn write_sync(&mut self) -> io::Result<()> {
517        let &mut Self {
518            ref mut modified,
519            ref mut files,
520        } = self;
521        modified
522            .drain(..)
523            .try_for_each(|id| files[id as usize].write_sync())
524    }
525}
526
527fn scan_inner<P: AsRef<Path>>(dir: P) -> Result<SourcesLists, SourcesListError> {
528    let paths = sources_list(dir)?;
529
530    SourcesLists::new_from_paths(paths.iter())
531}
532
533pub(crate) fn sources_list<P: AsRef<Path>>(dir: P) -> Result<Vec<PathBuf>, SourcesListError> {
534    let dir = dir.as_ref();
535    let mut paths = vec![];
536    let default = dir.join("etc/apt/sources.list");
537
538    if default.exists() {
539        paths.push(default);
540    }
541
542    if dir.join("etc/apt/sources.list.d/").exists() {
543        for entry in fs::read_dir(dir.join("etc/apt/sources.list.d/"))? {
544            let entry = entry?;
545            let path = entry.path();
546            if path
547                .extension()
548                .map_or(false, |e| e == "list" || e == "sources")
549            {
550                paths.push(path);
551            }
552        }
553    }
554
555    Ok(paths)
556}
557
558fn add_modified(modified: &mut Vec<u16>, list: u16) {
559    if !modified.iter().any(|&v| v == list) {
560        modified.push(list);
561    }
562}