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