]> git.proxmox.com Git - proxmox-apt.git/blob - src/repositories/file/sources_parser.rs
e824f3d2b40f3b5c7c5145196300291994d09eee
[proxmox-apt.git] / src / repositories / file / sources_parser.rs
1 use std::convert::TryInto;
2 use std::io::BufRead;
3 use std::iter::Iterator;
4
5 use anyhow::{bail, Error};
6
7 use crate::repositories::{
8 APTRepository, APTRepositoryFileType, APTRepositoryOption, APTRepositoryPackageType,
9 };
10
11 use super::APTRepositoryParser;
12
13 pub struct APTSourcesFileParser<R: BufRead> {
14 input: R,
15 stanza_nr: usize,
16 comment: String,
17 }
18
19 /// See `man sources.list` and `man deb822` for the format specification.
20 impl<R: BufRead> APTSourcesFileParser<R> {
21 pub fn new(reader: R) -> Self {
22 Self {
23 input: reader,
24 stanza_nr: 1,
25 comment: String::new(),
26 }
27 }
28
29 /// Based on APT's `StringToBool` in `strutl.cc`
30 fn string_to_bool(string: &str, default: bool) -> bool {
31 let string = string.trim_matches(|c| char::is_ascii_whitespace(&c));
32 let string = string.to_lowercase();
33
34 match &string[..] {
35 "1" | "yes" | "true" | "with" | "on" | "enable" => true,
36 "0" | "no" | "false" | "without" | "off" | "disable" => false,
37 _ => default,
38 }
39 }
40
41 /// Checks if `key` is valid according to deb822
42 fn valid_key(key: &str) -> bool {
43 if key.starts_with('-') {
44 return false;
45 };
46 return key.chars().all(|c| matches!(c, '!'..='9' | ';'..='~'));
47 }
48
49 /// Try parsing a repository in stanza format from `lines`.
50 ///
51 /// Returns `Ok(None)` when no stanza can be found.
52 ///
53 /// Comments are added to `self.comments`. If a stanza can be found,
54 /// `self.comment` is added to the repository's `comment` property.
55 ///
56 /// Fully commented out stanzas are treated as comments.
57 fn parse_stanza(&mut self, lines: &str) -> Result<Option<APTRepository>, Error> {
58 let mut repo = APTRepository::new(APTRepositoryFileType::Sources);
59
60 // Values may be folded into multiple lines.
61 // Those lines have to start with a space or a tab.
62 let lines = lines.replace("\n ", " ");
63 let lines = lines.replace("\n\t", " ");
64
65 let mut got_something = false;
66
67 for line in lines.lines() {
68 let line = line.trim_matches(|c| char::is_ascii_whitespace(&c));
69 if line.is_empty() {
70 continue;
71 }
72
73 if let Some(commented_out) = line.strip_prefix('#') {
74 self.comment = format!("{}{}\n", self.comment, commented_out);
75 continue;
76 }
77
78 if let Some(mid) = line.find(':') {
79 let (key, value_str) = line.split_at(mid);
80 let value_str = &value_str[1..];
81 let key = key.trim_matches(|c| char::is_ascii_whitespace(&c));
82
83 if key.is_empty() {
84 bail!("option has no key: '{}'", line);
85 }
86
87 if value_str.is_empty() {
88 // ignored by APT
89 eprintln!("option has no value: '{}'", line);
90 continue;
91 }
92
93 if !Self::valid_key(key) {
94 // ignored by APT
95 eprintln!("option with invalid key '{}'", key);
96 continue;
97 }
98
99 let values: Vec<String> = value_str
100 .split_ascii_whitespace()
101 .map(|value| value.to_string())
102 .collect();
103
104 match &key.to_lowercase()[..] {
105 "types" => {
106 if !repo.types.is_empty() {
107 eprintln!("key 'Types' was defined twice");
108 }
109 let mut types = Vec::<APTRepositoryPackageType>::new();
110 for package_type in values {
111 types.push((&package_type[..]).try_into()?);
112 }
113 repo.types = types;
114 }
115 "uris" => {
116 if !repo.uris.is_empty() {
117 eprintln!("key 'URIs' was defined twice");
118 }
119 repo.uris = values;
120 }
121 "suites" => {
122 if !repo.suites.is_empty() {
123 eprintln!("key 'Suites' was defined twice");
124 }
125 repo.suites = values;
126 }
127 "components" => {
128 if !repo.components.is_empty() {
129 eprintln!("key 'Components' was defined twice");
130 }
131 repo.components = values;
132 }
133 "enabled" => {
134 repo.set_enabled(Self::string_to_bool(value_str, true));
135 }
136 _ => repo.options.push(APTRepositoryOption {
137 key: key.to_string(),
138 values,
139 }),
140 }
141 } else {
142 bail!("got invalid line - '{:?}'", line);
143 }
144
145 got_something = true;
146 }
147
148 if !got_something {
149 return Ok(None);
150 }
151
152 repo.comment = std::mem::take(&mut self.comment);
153
154 Ok(Some(repo))
155 }
156
157 /// Helper function for `parse_repositories`.
158 fn try_parse_stanza(
159 &mut self,
160 lines: &str,
161 repos: &mut Vec<APTRepository>,
162 ) -> Result<(), Error> {
163 match self.parse_stanza(lines) {
164 Ok(Some(repo)) => {
165 repos.push(repo);
166 self.stanza_nr += 1;
167 }
168 Ok(None) => (),
169 Err(err) => bail!("malformed entry in stanza {} - {}", self.stanza_nr, err),
170 }
171
172 Ok(())
173 }
174 }
175
176 impl<R: BufRead> APTRepositoryParser for APTSourcesFileParser<R> {
177 fn parse_repositories(&mut self) -> Result<Vec<APTRepository>, Error> {
178 let mut repos = vec![];
179 let mut lines = String::new();
180
181 loop {
182 let old_length = lines.len();
183 match self.input.read_line(&mut lines) {
184 Err(err) => bail!("input error - {}", err),
185 Ok(0) => {
186 self.try_parse_stanza(&lines[..], &mut repos)?;
187 break;
188 }
189 Ok(_) => {
190 if (&lines[old_length..])
191 .trim_matches(|c| char::is_ascii_whitespace(&c))
192 .is_empty()
193 {
194 // detected end of stanza
195 self.try_parse_stanza(&lines[..], &mut repos)?;
196 lines.clear();
197 }
198 }
199 }
200 }
201
202 Ok(repos)
203 }
204 }