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