]> git.proxmox.com Git - proxmox-apt.git/blob - src/repositories/file/sources_parser.rs
clippy fixes
[proxmox-apt.git] / src / repositories / file / sources_parser.rs
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(_) => {
189 if (lines[old_length..])
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 }