pacmanconf/
pacmanconf.rs

1use cini::{Callback, CallbackKind, Ini};
2use std::str;
3use std::str::FromStr;
4use std::{ffi::OsStr, process::Command};
5
6use crate::error::{Error, ErrorKind, ErrorLine};
7
8/// A Pacman repository.
9///
10/// See pacman.conf (5) for information on each field.
11#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
12#[non_exhaustive]
13pub struct Repository {
14    /// Name
15    pub name: String,
16    /// Servers
17    pub servers: Vec<String>,
18    /// SigLevel
19    pub sig_level: Vec<String>,
20    /// Usage
21    pub usage: Vec<String>,
22}
23
24/// A pacman config.
25///
26/// See pacman.conf (5) for information on each field.
27#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
28#[non_exhaustive]
29pub struct Config {
30    /// RootDir
31    pub root_dir: String,
32    /// DBPath
33    pub db_path: String,
34    /// CacheDir
35    pub cache_dir: Vec<String>,
36    /// HookDir
37    pub hook_dir: Vec<String>,
38    /// GPGDir
39    pub gpg_dir: String,
40    /// LogFile
41    pub log_file: String,
42    /// HoldPkg
43    pub hold_pkg: Vec<String>,
44    /// IgnorePkg
45    pub ignore_pkg: Vec<String>,
46    /// IgnoreGroup
47    pub ignore_group: Vec<String>,
48    /// Architecture
49    pub architecture: Vec<String>,
50    /// XferCommand
51    pub xfer_command: String,
52    /// NoUpgrade
53    pub no_upgrade: Vec<String>,
54    /// NoExtract
55    pub no_extract: Vec<String>,
56    /// CleanMethod
57    pub clean_method: Vec<String>,
58    /// SigLevel
59    pub sig_level: Vec<String>,
60    /// LocalFileSigLevel
61    pub local_file_sig_level: Vec<String>,
62    /// RemoteFileSigLevel
63    pub remote_file_sig_level: Vec<String>,
64    /// DownloadUser
65    pub download_user: Option<String>,
66    /// UseSyslog
67    pub use_syslog: bool,
68    /// Color
69    pub color: bool,
70    /// UseDelta
71    pub use_delta: f64,
72    /// TotalDownload
73    pub total_download: bool,
74    /// CheckSpace
75    pub check_space: bool,
76    /// VerpsePkgLists
77    pub verbose_pkg_lists: bool,
78    /// DisableDownloadTimeout
79    pub disable_download_timeout: bool,
80    /// ParallelDownloads
81    pub parallel_downloads: u64,
82    /// DisableSandbox
83    pub disable_sandbox: bool,
84    /// ILoveCandy
85    pub chomp: bool,
86    /// \[repo_name\]
87    pub repos: Vec<Repository>,
88}
89
90#[doc(hidden)]
91impl Ini for Config {
92    type Err = Error;
93
94    fn callback(&mut self, cb: Callback) -> Result<(), Self::Err> {
95        let line = Some(ErrorLine::new(cb.line_number, cb.line));
96
97        match cb.kind {
98            CallbackKind::Section(section) => {
99                self.handle_section(section);
100            }
101            CallbackKind::Directive(section, key, value) => {
102                self.handle_directive(section, key, value)
103                    .map_err(|kind| Error { kind, line })?;
104            }
105        }
106
107        Ok(())
108    }
109}
110
111impl FromStr for Config {
112    type Err = Error;
113
114    fn from_str(s: &str) -> Result<Self, Self::Err> {
115        let mut config = Config::default();
116        config.parse_str(s)?;
117        Ok(config)
118    }
119}
120
121impl Config {
122    /// Creates a new Config from the default pacman.conf.
123    ///
124    /// The default pacman.conf location is a compile time option of
125    /// pacman but is usually located at /etc/pacman.conf.
126    pub fn new() -> Result<Config, Error> {
127        Self::with_opts::<&OsStr>(None, None, None)
128    }
129
130    /// Creates a new Config using pacman's compiled in defaults.
131    ///
132    /// Parsing an empty file causes pacman-conf to fill in each
133    /// field with pacman's compiled in default values. This should
134    /// not be confused with the `Default::default()` function which
135    /// is derived and will give rust's default values eg:
136    /// empty string, 0, etc.
137    pub fn empty() -> Result<Config, Error> {
138        Self::from_file("/dev/null")
139    }
140
141    /// Create a new Config from a file.
142    #[doc(hidden)]
143    pub fn from_file<T: AsRef<OsStr>>(config: T) -> Result<Config, Error> {
144        Self::with_opts(None, Some(config), None)
145    }
146
147    /// Create a new Config with options.
148    ///
149    /// - bin: The location of the `pacman-conf` binary. Default is
150    /// `pacman-conf` in PATH.
151    /// - config: Location of config file to parse: Default is
152    /// pacman's compiled in default (usually /etc/pacman.conf).
153    /// root_dir: The RootDir: Default is pacman's compiled in
154    /// default (usually /).
155    #[doc(hidden)]
156    pub fn with_opts<T: AsRef<OsStr>>(
157        bin: Option<T>,
158        config: Option<T>,
159        root_dir: Option<T>,
160    ) -> Result<Config, Error> {
161        let str = Self::expand_with_opts(bin, config, root_dir)?;
162        let mut config = Config::default();
163        config.parse_str(&str)?;
164        Ok(config)
165    }
166
167    /// Expand the pacman_conf
168    ///
169    /// This generates a pacman.conf with all the Includes expanded
170    ///
171    /// - bin: The location of the `pacman-conf` binary. Default is
172    /// `pacman-conf` in PATH.
173    /// - config: Location of config file to parse: Default is
174    /// pacman's compiled in default (usually /etc/pacman.conf).
175    /// root_dir: The RootDir: Default is pacman's compiled in
176    /// default (usually /).
177    #[doc(hidden)]
178    pub fn expand_with_opts<T: AsRef<OsStr>>(
179        bin: Option<T>,
180        config: Option<T>,
181        root_dir: Option<T>,
182    ) -> Result<String, Error> {
183        let cmd = bin
184            .as_ref()
185            .map(|t| t.as_ref())
186            .unwrap_or_else(|| OsStr::new("pacman-conf"));
187        let mut cmd = Command::new(cmd);
188        if let Some(root) = root_dir {
189            cmd.arg("--root").arg(root);
190        }
191        if let Some(config) = config {
192            cmd.arg("--config").arg(config);
193        }
194
195        let output = cmd.output()?;
196
197        if !output.status.success() {
198            return Err(ErrorKind::Runtime(
199                String::from_utf8(output.stderr).map_err(|e| e.utf8_error())?,
200            )
201            .into());
202        }
203
204        let mut str = String::from_utf8(output.stdout).map_err(|e| e.utf8_error())?;
205        if str.ends_with('\n') {
206            str.pop().unwrap();
207        }
208        Ok(str)
209    }
210
211    /// Expand the pacman_conf
212    ///
213    /// This generates a pacman.conf with all the Includes expanded
214    #[doc(hidden)]
215    pub fn expand_from_file<T: AsRef<OsStr>>(config: T) -> Result<String, Error> {
216        Self::expand_with_opts(None, Some(config), None)
217    }
218
219    fn handle_section(&mut self, section: &str) {
220        if section != "options" {
221            self.repos.push(Repository {
222                name: section.into(),
223                ..Default::default()
224            });
225        }
226    }
227
228    fn handle_directive(
229        &mut self,
230        section: Option<&str>,
231        key: &str,
232        value: Option<&str>,
233    ) -> Result<(), ErrorKind> {
234        if let Some(section) = section {
235            if section == "options" {
236                self.handle_option(section, key, value)
237            } else {
238                self.handle_repo(section, key, value)
239            }
240        } else {
241            Err(ErrorKind::NoSection(key.into()))
242        }
243    }
244
245    fn handle_repo(
246        &mut self,
247        section: &str,
248        key: &str,
249        value: Option<&str>,
250    ) -> Result<(), ErrorKind> {
251        let repo = &mut self.repos.iter_mut().last().unwrap();
252        let value = value.ok_or_else(|| ErrorKind::MissingValue(section.into(), key.into()));
253
254        match key {
255            "Server" => repo.servers.push(value?.into()),
256            "SigLevel" => repo.sig_level.push(value?.into()),
257            "Usage" => repo.usage.push(value?.into()),
258            _ => (),
259        }
260
261        Ok(())
262    }
263
264    fn handle_option(
265        &mut self,
266        section: &str,
267        key: &str,
268        value: Option<&str>,
269    ) -> Result<(), ErrorKind> {
270        if let Some(value) = value {
271            match key {
272                "RootDir" => self.root_dir = value.into(),
273                "DBPath" => self.db_path = value.into(),
274                "CacheDir" => self.cache_dir.push(value.into()),
275                "HookDir" => self.hook_dir.push(value.into()),
276                "GPGDir" => self.gpg_dir = value.into(),
277                "LogFile" => self.log_file = value.into(),
278                "HoldPkg" => self.hold_pkg.push(value.into()),
279                "IgnorePkg" => self.ignore_pkg.push(value.into()),
280                "IgnoreGroup" => self.ignore_group.push(value.into()),
281                "Architecture" => self.architecture.push(value.into()),
282                "XferCommand" => self.xfer_command = value.into(),
283                "NoUpgrade" => self.no_upgrade.push(value.into()),
284                "NoExtract" => self.no_extract.push(value.into()),
285                "CleanMethod" => self.clean_method.push(value.into()),
286                "SigLevel" => self.sig_level.push(value.into()),
287                "LocalFileSigLevel" => self.local_file_sig_level.push(value.into()),
288                "RemoteFileSigLevel" => self.remote_file_sig_level.push(value.into()),
289                "UseDelta" => {
290                    self.use_delta = value.parse().map_err(|_| {
291                        ErrorKind::InvalidValue(section.into(), key.into(), value.into())
292                    })?
293                }
294                "ParallelDownloads" => {
295                    self.parallel_downloads = value.parse().map_err(|_| {
296                        ErrorKind::InvalidValue(section.into(), key.into(), value.into())
297                    })?
298                }
299                "DownloadUser" => self.download_user = Some(value.into()),
300
301                _ => (),
302            };
303        } else {
304            match key {
305                "Color" => self.color = true,
306                "UseSyslog" => self.use_syslog = true,
307                "TotalDownload" => self.total_download = true,
308                "CheckSpace" => self.check_space = true,
309                "VerbosePkgLists" => self.verbose_pkg_lists = true,
310                "DisableDownloadTimeout" => self.disable_download_timeout = true,
311                "UseDelta" => self.use_delta = 0.7,
312                "DisableSandbox" => self.disable_sandbox = true,
313                "ILoveCandy" => self.chomp = true,
314                _ => (),
315            };
316        }
317
318        Ok(())
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325    use std::path::Path;
326
327    #[test]
328    fn eq_pacman_conf() {
329        let pacman_conf = Config {
330            root_dir: "/".into(),
331            db_path: "/var/lib/pacman/".into(),
332            cache_dir: vec!["/var/cache/pacman/pkg/".into()],
333            hook_dir: vec!["/etc/pacman.d/hooks/".into()],
334            gpg_dir: "/etc/pacman.d/gnupg/".into(),
335            log_file: "/var/log/pacman.log".into(),
336            hold_pkg: vec!["pacman".into(), "glibc".into()],
337            ignore_pkg: vec![
338                "linux-ck-headers".into(),
339                "linux-ck".into(),
340                "vim-youcompleteme*".into(),
341                "brackets-bin".into(),
342            ],
343            ignore_group: vec![],
344            architecture: vec!["x86_64".into()],
345            xfer_command: "".into(),
346            no_upgrade: vec![],
347            no_extract: vec![],
348            clean_method: vec!["KeepInstalled".into()],
349            sig_level: vec![
350                "PackageRequired".into(),
351                "PackageTrustedOnly".into(),
352                "DatabaseOptional".into(),
353                "DatabaseTrustedOnly".into(),
354            ],
355            local_file_sig_level: vec!["PackageOptional".into(), "PackageTrustedOnly".into()],
356            remote_file_sig_level: vec!["PackageRequired".into(), "PackageTrustedOnly".into()],
357            download_user: Some("foo".to_string()),
358            use_syslog: false,
359            color: true,
360            use_delta: 0.0,
361            total_download: false,
362            parallel_downloads: 1,
363            check_space: true,
364            verbose_pkg_lists: true,
365            disable_download_timeout: false,
366            disable_sandbox: true,
367            chomp: true,
368            repos: vec![
369                Repository {
370                    name: "testing".into(),
371                    servers: vec![
372                        "http://mirror.cyberbits.eu/archlinux/testing/os/x86_64".into(),
373                        "https://ftp.halifax.rwth-aachen.de/archlinux/testing/os/x86_64".into(),
374                        "https://mirror.cyberbits.eu/archlinux/testing/os/x86_64".into(),
375                        "rsync://ftp.halifax.rwth-aachen.de/archlinux/testing/os/x86_64".into(),
376                        "http://mirrors.neusoft.edu.cn/archlinux/testing/os/x86_64".into(),
377                    ],
378                    sig_level: vec![],
379                    usage: vec!["All".into()],
380                },
381                Repository {
382                    name: "core".into(),
383                    servers: vec![
384                        "http://mirror.cyberbits.eu/archlinux/core/os/x86_64".into(),
385                        "https://ftp.halifax.rwth-aachen.de/archlinux/core/os/x86_64".into(),
386                        "https://mirror.cyberbits.eu/archlinux/core/os/x86_64".into(),
387                        "rsync://ftp.halifax.rwth-aachen.de/archlinux/core/os/x86_64".into(),
388                        "http://mirrors.neusoft.edu.cn/archlinux/core/os/x86_64".into(),
389                    ],
390                    sig_level: vec![],
391                    usage: vec!["All".into()],
392                },
393                Repository {
394                    name: "extra".into(),
395                    servers: vec![
396                        "http://mirror.cyberbits.eu/archlinux/extra/os/x86_64".into(),
397                        "https://ftp.halifax.rwth-aachen.de/archlinux/extra/os/x86_64".into(),
398                        "https://mirror.cyberbits.eu/archlinux/extra/os/x86_64".into(),
399                        "rsync://ftp.halifax.rwth-aachen.de/archlinux/extra/os/x86_64".into(),
400                        "http://mirrors.neusoft.edu.cn/archlinux/extra/os/x86_64".into(),
401                    ],
402                    sig_level: vec![],
403                    usage: vec!["All".into()],
404                },
405                Repository {
406                    name: "community-testing".into(),
407                    servers: vec![
408                        "http://mirror.cyberbits.eu/archlinux/community-testing/os/x86_64".into(),
409                        "https://ftp.halifax.rwth-aachen.de/archlinux/community-testing/os/x86_64"
410                            .into(),
411                        "https://mirror.cyberbits.eu/archlinux/community-testing/os/x86_64".into(),
412                        "rsync://ftp.halifax.rwth-aachen.de/archlinux/community-testing/os/x86_64"
413                            .into(),
414                        "http://mirrors.neusoft.edu.cn/archlinux/community-testing/os/x86_64"
415                            .into(),
416                    ],
417                    sig_level: vec![],
418                    usage: vec!["All".into()],
419                },
420                Repository {
421                    name: "community".into(),
422                    servers: vec![
423                        "http://mirror.cyberbits.eu/archlinux/community/os/x86_64".into(),
424                        "https://ftp.halifax.rwth-aachen.de/archlinux/community/os/x86_64".into(),
425                        "https://mirror.cyberbits.eu/archlinux/community/os/x86_64".into(),
426                        "rsync://ftp.halifax.rwth-aachen.de/archlinux/community/os/x86_64".into(),
427                        "http://mirrors.neusoft.edu.cn/archlinux/community/os/x86_64".into(),
428                    ],
429                    sig_level: vec![],
430                    usage: vec!["All".into()],
431                },
432                Repository {
433                    name: "multilib-testing".into(),
434                    servers: vec![
435                        "http://mirror.cyberbits.eu/archlinux/multilib-testing/os/x86_64".into(),
436                        "https://ftp.halifax.rwth-aachen.de/archlinux/multilib-testing/os/x86_64"
437                            .into(),
438                        "https://mirror.cyberbits.eu/archlinux/multilib-testing/os/x86_64".into(),
439                        "rsync://ftp.halifax.rwth-aachen.de/archlinux/multilib-testing/os/x86_64"
440                            .into(),
441                        "http://mirrors.neusoft.edu.cn/archlinux/multilib-testing/os/x86_64".into(),
442                    ],
443                    sig_level: vec![],
444                    usage: vec!["All".into()],
445                },
446                Repository {
447                    name: "multilib".into(),
448                    servers: vec![
449                        "http://mirror.cyberbits.eu/archlinux/multilib/os/x86_64".into(),
450                        "https://ftp.halifax.rwth-aachen.de/archlinux/multilib/os/x86_64".into(),
451                        "https://mirror.cyberbits.eu/archlinux/multilib/os/x86_64".into(),
452                        "rsync://ftp.halifax.rwth-aachen.de/archlinux/multilib/os/x86_64".into(),
453                        "http://mirrors.neusoft.edu.cn/archlinux/multilib/os/x86_64".into(),
454                    ],
455                    sig_level: vec![],
456                    usage: vec!["All".into()],
457                },
458            ],
459        };
460
461        assert_eq!(
462            pacman_conf.repos,
463            Config::from_file("tests/pacman.conf").unwrap().repos
464        );
465        assert_eq!(pacman_conf, Config::from_file("tests/pacman.conf").unwrap());
466    }
467
468    #[test]
469    fn test_success() {
470        Config::new().unwrap();
471        Config::empty().unwrap();
472        Config::with_opts::<&OsStr>(None, None, None).unwrap();
473        Config::with_opts(None, Some("tests/pacman.conf"), None).unwrap();
474        Config::with_opts(None, Some(Path::new("tests/pacman.conf")), None).unwrap();
475        Config::from_file("tests/pacman.conf").unwrap();
476    }
477
478    #[test]
479    fn test_error() {
480        let err = Config::from_str(
481            "
482                                    [options]
483                                    Color
484                                    [repo]
485                                    Server
486                                    ",
487        )
488        .unwrap_err();
489
490        if let ErrorKind::MissingValue(s, k) = err.kind {
491            assert_eq!(s, "repo");
492            assert_eq!(k, "Server");
493            assert_eq!(err.line.unwrap().number, 5);
494        } else {
495            panic!("Error kind is not MissingValue");
496        }
497    }
498}