]>
git.proxmox.com Git - proxmox-apt.git/blob - src/repositories/file.rs
3df59db3801d08c6aadf39c14f02549868b263ac
1 use std
::convert
::{TryFrom, TryInto}
;
3 use std
::path
::{Path, PathBuf}
;
5 use anyhow
::{format_err, Error}
;
6 use serde
::{Deserialize, Serialize}
;
8 use crate::repositories
::release
::DebianCodename
;
9 use crate::repositories
::repository
::{
10 APTRepository
, APTRepositoryFileType
, APTRepositoryPackageType
,
13 use proxmox
::api
::api
;
16 use list_parser
::APTListFileParser
;
19 use sources_parser
::APTSourcesFileParser
;
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
>;
30 type: APTRepositoryFileType
,
33 description
: "List of APT repositories.",
40 description
: "Digest for the content of the file.",
44 description
: "Digest byte.",
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.
57 /// The type of the file.
58 pub file_type
: APTRepositoryFileType
,
60 /// List of repositories in the file.
61 pub repositories
: Vec
<APTRepository
>,
63 /// Digest of the original contents.
64 pub digest
: Option
<[u8; 32]>,
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.
75 /// The error message.
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
)
85 impl std
::error
::Error
for APTRepositoryFileError
{
86 fn source(&self) -> Option
<&(dyn std
::error
::Error
+ '
static)> {
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")]
100 /// Index of the associated respository within the file (starting from 0).
103 /// The property from which the info originates (e.g. "Suites")
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub property
: Option
<String
>,
107 /// Info kind (e.g. "warning")
114 impl APTRepositoryFile
{
115 /// Creates a new `APTRepositoryFile` without parsing.
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();
123 let new_err
= |path_string
: String
, err
: &str| APTRepositoryFileError
{
125 error
: err
.to_string(),
128 let path_string
= path
132 .map_err(|os_string
| {
134 os_string
.to_string_lossy().to_string(),
135 "path is not valid unicode",
139 let new_err
= |err
| new_err(path_string
.clone(), err
);
145 let file_name
= match path
.file_name() {
146 Some(file_name
) => file_name
149 .map_err(|_
| new_err("invalid path"))?
,
150 None
=> return Err(new_err("invalid path")),
153 if file_name
.starts_with('
.'
) || file_name
.ends_with('
~'
) {
157 let extension
= match path
.extension() {
158 Some(extension
) => extension
161 .map_err(|_
| new_err("invalid path"))?
,
162 None
=> return Err(new_err("invalid extension")),
165 // See APT's apt-pkg/init.cc
166 if extension
.starts_with("dpkg-")
167 || extension
.starts_with("ucf-")
170 "disabled" | "bak" | "save" | "orig" | "distUpgrade"
176 let file_type
= APTRepositoryFileType
::try_from(&extension
[..])
177 .map_err(|_
| new_err("invalid extension"))?
;
181 .all(|x
| x
.is_ascii_alphanumeric() || x
== '_'
|| x
== '
-'
|| x
== '
.'
)
183 return Err(new_err("invalid characters in file name"));
189 repositories
: vec
![],
194 /// Check if the file exists.
195 pub fn exists(&self) -> bool
{
196 PathBuf
::from(&self.path
).exists()
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
)))?
;
202 let digest
= openssl
::sha
::sha256(&content
);
204 Ok((content
, digest
))
207 /// Create an `APTRepositoryFileError`.
208 pub fn err(&self, error
: Error
) -> APTRepositoryFileError
{
209 APTRepositoryFileError
{
210 path
: self.path
.clone(),
211 error
: error
.to_string(),
215 /// Parses the APT repositories configured in the file on disk, including
218 /// Resets the current repositories and digest, even on failure.
219 pub fn parse(&mut self) -> Result
<(), APTRepositoryFileError
> {
220 self.repositories
.clear();
223 let (content
, digest
) = self.read_with_digest()?
;
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
[..])),
230 let repos
= parser
.parse_repositories().map_err(|err
| self.err(err
))?
;
232 for (n
, repo
) in repos
.iter().enumerate() {
234 .map_err(|err
| self.err(format_err
!("check for repository {} - {}", n
+ 1, err
)))?
;
237 self.repositories
= repos
;
238 self.digest
= Some(digest
);
243 /// Writes the repositories to the file on disk.
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
{
250 return Err(self.err(format_err
!("digest specified, but file does not exist")));
253 let (_
, current_digest
) = self.read_with_digest()?
;
254 if digest
!= current_digest
{
255 return Err(self.err(format_err
!("digest mismatch")));
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
)));
264 let mut content
= vec
![];
266 for (n
, repo
) in self.repositories
.iter().enumerate() {
268 .map_err(|err
| self.err(format_err
!("check for repository {} - {}", n
+ 1, err
)))?
;
270 repo
.write(&mut content
)
271 .map_err(|err
| self.err(format_err
!("writing repository {} - {}", n
+ 1, err
)))?
;
274 let path
= PathBuf
::from(&self.path
);
275 let dir
= match path
.parent() {
277 None
=> return Err(self.err(format_err
!("invalid path"))),
280 std
::fs
::create_dir_all(dir
)
281 .map_err(|err
| self.err(format_err
!("unable to create parent dir - {}", err
)))?
;
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
));
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
)));
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
)));
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
![];
306 for (n
, repo
) in self.repositories
.iter().enumerate() {
307 if !repo
.types
.contains(&APTRepositoryPackageType
::Deb
) {
311 let mut add_info
= |kind
: &str, message
| {
312 infos
.push(APTRepositoryInfo
{
313 path
: self.path
.clone(),
315 property
: Some("Suites".to_string()),
316 kind
: kind
.to_string(),
321 let message_old
= |suite
| format
!("old suite '{}' configured!", suite
);
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'!";
326 for suite
in repo
.suites
.iter() {
327 let base_suite
= suite_variant(suite
).0;
330 "oldoldstable" | "oldstable" => {
331 add_info("warning", message_old(base_suite
));
333 "testing" | "unstable" | "experimental" | "sid" => {
334 add_info("warning", message_new(base_suite
));
337 add_info("warning", message_stable
.to_string());
342 let codename
: DebianCodename
= match base_suite
.try_into() {
343 Ok(codename
) => codename
,
347 if codename
< current_codename
{
348 add_info("warning", message_old(base_suite
));
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
));
362 /// Checks for official URIs.
363 pub fn check_uris(&self) -> Vec
<APTRepositoryInfo
> {
364 let mut infos
= vec
![];
366 for (n
, repo
) in self.repositories
.iter().enumerate() {
367 let mut origin
= match repo
.get_cached_origin() {
368 Ok(option
) => option
,
372 if origin
.is_none() {
373 origin
= repo
.origin_from_uris();
376 if let Some(origin
) = origin
{
377 infos
.push(APTRepositoryInfo
{
378 path
: self.path
.clone(),
380 kind
: "origin".to_string(),
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
..]),