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#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
12#[non_exhaustive]
13pub struct Repository {
14 pub name: String,
16 pub servers: Vec<String>,
18 pub sig_level: Vec<String>,
20 pub usage: Vec<String>,
22}
23
24#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
28#[non_exhaustive]
29pub struct Config {
30 pub root_dir: String,
32 pub db_path: String,
34 pub cache_dir: Vec<String>,
36 pub hook_dir: Vec<String>,
38 pub gpg_dir: String,
40 pub log_file: String,
42 pub hold_pkg: Vec<String>,
44 pub ignore_pkg: Vec<String>,
46 pub ignore_group: Vec<String>,
48 pub architecture: Vec<String>,
50 pub xfer_command: String,
52 pub no_upgrade: Vec<String>,
54 pub no_extract: Vec<String>,
56 pub clean_method: Vec<String>,
58 pub sig_level: Vec<String>,
60 pub local_file_sig_level: Vec<String>,
62 pub remote_file_sig_level: Vec<String>,
64 pub download_user: Option<String>,
66 pub use_syslog: bool,
68 pub color: bool,
70 pub use_delta: f64,
72 pub total_download: bool,
74 pub check_space: bool,
76 pub verbose_pkg_lists: bool,
78 pub disable_download_timeout: bool,
80 pub parallel_downloads: u64,
82 pub disable_sandbox: bool,
84 pub chomp: bool,
86 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 pub fn new() -> Result<Config, Error> {
127 Self::with_opts::<&OsStr>(None, None, None)
128 }
129
130 pub fn empty() -> Result<Config, Error> {
138 Self::from_file("/dev/null")
139 }
140
141 #[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 #[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 #[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 #[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}