]> git.proxmox.com Git - proxmox-apt.git/blob - src/repositories/file.rs
3df59db3801d08c6aadf39c14f02549868b263ac
[proxmox-apt.git] / src / repositories / file.rs
1 use std::convert::{TryFrom, TryInto};
2 use std::fmt::Display;
3 use std::path::{Path, PathBuf};
4
5 use anyhow::{format_err, Error};
6 use serde::{Deserialize, Serialize};
7
8 use crate::repositories::release::DebianCodename;
9 use crate::repositories::repository::{
10 APTRepository, APTRepositoryFileType, APTRepositoryPackageType,
11 };
12
13 use proxmox::api::api;
14
15 mod list_parser;
16 use list_parser::APTListFileParser;
17
18 mod sources_parser;
19 use sources_parser::APTSourcesFileParser;
20
21 trait APTRepositoryParser {
22 /// Parse all repositories including the disabled ones and push them onto
23 /// the provided vector.
24 fn parse_repositories(&mut self) -> Result<Vec<APTRepository>, Error>;
25 }
26
27 #[api(
28 properties: {
29 "file-type": {
30 type: APTRepositoryFileType,
31 },
32 repositories: {
33 description: "List of APT repositories.",
34 type: Array,
35 items: {
36 type: APTRepository,
37 },
38 },
39 digest: {
40 description: "Digest for the content of the file.",
41 optional: true,
42 type: Array,
43 items: {
44 description: "Digest byte.",
45 type: Integer,
46 },
47 },
48 },
49 )]
50 #[derive(Debug, Clone, Serialize, Deserialize)]
51 #[serde(rename_all = "kebab-case")]
52 /// Represents an abstract APT repository file.
53 pub struct APTRepositoryFile {
54 /// The path to the file.
55 pub path: String,
56
57 /// The type of the file.
58 pub file_type: APTRepositoryFileType,
59
60 /// List of repositories in the file.
61 pub repositories: Vec<APTRepository>,
62
63 /// Digest of the original contents.
64 pub digest: Option<[u8; 32]>,
65 }
66
67 #[api]
68 #[derive(Debug, Clone, Serialize, Deserialize)]
69 #[serde(rename_all = "kebab-case")]
70 /// Error type for problems with APT repository files.
71 pub struct APTRepositoryFileError {
72 /// The path to the problematic file.
73 pub path: String,
74
75 /// The error message.
76 pub error: String,
77 }
78
79 impl Display for APTRepositoryFileError {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 write!(f, "proxmox-apt error for '{}' - {}", self.path, self.error)
82 }
83 }
84
85 impl std::error::Error for APTRepositoryFileError {
86 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
87 None
88 }
89 }
90
91 #[api]
92 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
93 #[serde(rename_all = "kebab-case")]
94 /// Additional information for a repository.
95 pub struct APTRepositoryInfo {
96 /// Path to the defining file.
97 #[serde(skip_serializing_if = "String::is_empty")]
98 pub path: String,
99
100 /// Index of the associated respository within the file (starting from 0).
101 pub index: usize,
102
103 /// The property from which the info originates (e.g. "Suites")
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub property: Option<String>,
106
107 /// Info kind (e.g. "warning")
108 pub kind: String,
109
110 /// Info message
111 pub message: String,
112 }
113
114 impl APTRepositoryFile {
115 /// Creates a new `APTRepositoryFile` without parsing.
116 ///
117 /// If the file is hidden, the path points to a directory, or the extension
118 /// is usually ignored by APT (e.g. `.orig`), `Ok(None)` is returned, while
119 /// invalid file names yield an error.
120 pub fn new<P: AsRef<Path>>(path: P) -> Result<Option<Self>, APTRepositoryFileError> {
121 let path: PathBuf = path.as_ref().to_path_buf();
122
123 let new_err = |path_string: String, err: &str| APTRepositoryFileError {
124 path: path_string,
125 error: err.to_string(),
126 };
127
128 let path_string = path
129 .clone()
130 .into_os_string()
131 .into_string()
132 .map_err(|os_string| {
133 new_err(
134 os_string.to_string_lossy().to_string(),
135 "path is not valid unicode",
136 )
137 })?;
138
139 let new_err = |err| new_err(path_string.clone(), err);
140
141 if path.is_dir() {
142 return Ok(None);
143 }
144
145 let file_name = match path.file_name() {
146 Some(file_name) => file_name
147 .to_os_string()
148 .into_string()
149 .map_err(|_| new_err("invalid path"))?,
150 None => return Err(new_err("invalid path")),
151 };
152
153 if file_name.starts_with('.') || file_name.ends_with('~') {
154 return Ok(None);
155 }
156
157 let extension = match path.extension() {
158 Some(extension) => extension
159 .to_os_string()
160 .into_string()
161 .map_err(|_| new_err("invalid path"))?,
162 None => return Err(new_err("invalid extension")),
163 };
164
165 // See APT's apt-pkg/init.cc
166 if extension.starts_with("dpkg-")
167 || extension.starts_with("ucf-")
168 || matches!(
169 extension.as_str(),
170 "disabled" | "bak" | "save" | "orig" | "distUpgrade"
171 )
172 {
173 return Ok(None);
174 }
175
176 let file_type = APTRepositoryFileType::try_from(&extension[..])
177 .map_err(|_| new_err("invalid extension"))?;
178
179 if !file_name
180 .chars()
181 .all(|x| x.is_ascii_alphanumeric() || x == '_' || x == '-' || x == '.')
182 {
183 return Err(new_err("invalid characters in file name"));
184 }
185
186 Ok(Some(Self {
187 path: path_string,
188 file_type,
189 repositories: vec![],
190 digest: None,
191 }))
192 }
193
194 /// Check if the file exists.
195 pub fn exists(&self) -> bool {
196 PathBuf::from(&self.path).exists()
197 }
198
199 pub fn read_with_digest(&self) -> Result<(Vec<u8>, [u8; 32]), APTRepositoryFileError> {
200 let content = std::fs::read(&self.path).map_err(|err| self.err(format_err!("{}", err)))?;
201
202 let digest = openssl::sha::sha256(&content);
203
204 Ok((content, digest))
205 }
206
207 /// Create an `APTRepositoryFileError`.
208 pub fn err(&self, error: Error) -> APTRepositoryFileError {
209 APTRepositoryFileError {
210 path: self.path.clone(),
211 error: error.to_string(),
212 }
213 }
214
215 /// Parses the APT repositories configured in the file on disk, including
216 /// disabled ones.
217 ///
218 /// Resets the current repositories and digest, even on failure.
219 pub fn parse(&mut self) -> Result<(), APTRepositoryFileError> {
220 self.repositories.clear();
221 self.digest = None;
222
223 let (content, digest) = self.read_with_digest()?;
224
225 let mut parser: Box<dyn APTRepositoryParser> = match self.file_type {
226 APTRepositoryFileType::List => Box::new(APTListFileParser::new(&content[..])),
227 APTRepositoryFileType::Sources => Box::new(APTSourcesFileParser::new(&content[..])),
228 };
229
230 let repos = parser.parse_repositories().map_err(|err| self.err(err))?;
231
232 for (n, repo) in repos.iter().enumerate() {
233 repo.basic_check()
234 .map_err(|err| self.err(format_err!("check for repository {} - {}", n + 1, err)))?;
235 }
236
237 self.repositories = repos;
238 self.digest = Some(digest);
239
240 Ok(())
241 }
242
243 /// Writes the repositories to the file on disk.
244 ///
245 /// If a digest is provided, checks that the current content of the file still
246 /// produces the same one.
247 pub fn write(&self) -> Result<(), APTRepositoryFileError> {
248 if let Some(digest) = self.digest {
249 if !self.exists() {
250 return Err(self.err(format_err!("digest specified, but file does not exist")));
251 }
252
253 let (_, current_digest) = self.read_with_digest()?;
254 if digest != current_digest {
255 return Err(self.err(format_err!("digest mismatch")));
256 }
257 }
258
259 if self.repositories.is_empty() {
260 return std::fs::remove_file(&self.path)
261 .map_err(|err| self.err(format_err!("unable to remove file - {}", err)));
262 }
263
264 let mut content = vec![];
265
266 for (n, repo) in self.repositories.iter().enumerate() {
267 repo.basic_check()
268 .map_err(|err| self.err(format_err!("check for repository {} - {}", n + 1, err)))?;
269
270 repo.write(&mut content)
271 .map_err(|err| self.err(format_err!("writing repository {} - {}", n + 1, err)))?;
272 }
273
274 let path = PathBuf::from(&self.path);
275 let dir = match path.parent() {
276 Some(dir) => dir,
277 None => return Err(self.err(format_err!("invalid path"))),
278 };
279
280 std::fs::create_dir_all(dir)
281 .map_err(|err| self.err(format_err!("unable to create parent dir - {}", err)))?;
282
283 let pid = std::process::id();
284 let mut tmp_path = path.clone();
285 tmp_path.set_extension("tmp");
286 tmp_path.set_extension(format!("{}", pid));
287
288 if let Err(err) = std::fs::write(&tmp_path, content) {
289 let _ = std::fs::remove_file(&tmp_path);
290 return Err(self.err(format_err!("writing {:?} failed - {}", path, err)));
291 }
292
293 if let Err(err) = std::fs::rename(&tmp_path, &path) {
294 let _ = std::fs::remove_file(&tmp_path);
295 return Err(self.err(format_err!("rename failed for {:?} - {}", path, err)));
296 }
297
298 Ok(())
299 }
300
301 /// Checks if old or unstable suites are configured and also that the
302 /// `stable` keyword is not used.
303 pub fn check_suites(&self, current_codename: DebianCodename) -> Vec<APTRepositoryInfo> {
304 let mut infos = vec![];
305
306 for (n, repo) in self.repositories.iter().enumerate() {
307 if !repo.types.contains(&APTRepositoryPackageType::Deb) {
308 continue;
309 }
310
311 let mut add_info = |kind: &str, message| {
312 infos.push(APTRepositoryInfo {
313 path: self.path.clone(),
314 index: n,
315 property: Some("Suites".to_string()),
316 kind: kind.to_string(),
317 message,
318 })
319 };
320
321 let message_old = |suite| format!("old suite '{}' configured!", suite);
322 let message_new =
323 |suite| format!("suite '{}' should not be used in production!", suite);
324 let message_stable = "use the name of the stable distribution instead of 'stable'!";
325
326 for suite in repo.suites.iter() {
327 let base_suite = suite_variant(suite).0;
328
329 match base_suite {
330 "oldoldstable" | "oldstable" => {
331 add_info("warning", message_old(base_suite));
332 }
333 "testing" | "unstable" | "experimental" | "sid" => {
334 add_info("warning", message_new(base_suite));
335 }
336 "stable" => {
337 add_info("warning", message_stable.to_string());
338 }
339 _ => (),
340 };
341
342 let codename: DebianCodename = match base_suite.try_into() {
343 Ok(codename) => codename,
344 Err(_) => continue,
345 };
346
347 if codename < current_codename {
348 add_info("warning", message_old(base_suite));
349 }
350
351 if Some(codename) == current_codename.next() {
352 add_info("ignore-pre-upgrade-warning", message_new(base_suite));
353 } else if codename > current_codename {
354 add_info("warning", message_new(base_suite));
355 }
356 }
357 }
358
359 infos
360 }
361
362 /// Checks for official URIs.
363 pub fn check_uris(&self) -> Vec<APTRepositoryInfo> {
364 let mut infos = vec![];
365
366 for (n, repo) in self.repositories.iter().enumerate() {
367 let mut origin = match repo.get_cached_origin() {
368 Ok(option) => option,
369 Err(_) => None,
370 };
371
372 if origin.is_none() {
373 origin = repo.origin_from_uris();
374 }
375
376 if let Some(origin) = origin {
377 infos.push(APTRepositoryInfo {
378 path: self.path.clone(),
379 index: n,
380 kind: "origin".to_string(),
381 property: None,
382 message: origin,
383 });
384 }
385 }
386
387 infos
388 }
389 }
390
391 /// Splits the suite into its base part and variant.
392 /// Does not expect the base part to contain either `-` or `/`.
393 fn suite_variant(suite: &str) -> (&str, &str) {
394 match suite.find(&['-', '/'][..]) {
395 Some(n) => (&suite[0..n], &suite[n..]),
396 None => (suite, ""),
397 }
398 }