2 use std
::io
::{BufRead, BufReader, Write}
;
3 use std
::path
::PathBuf
;
5 use anyhow
::{bail, format_err, Error}
;
6 use serde
::{Deserialize, Serialize}
;
8 use proxmox_schema
::api
;
10 use crate::repositories
::standard
::APTRepositoryHandle
;
13 #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
14 #[serde(rename_all = "lowercase")]
15 pub enum APTRepositoryFileType
{
16 /// One-line-style format
18 /// DEB822-style format
22 impl TryFrom
<&str> for APTRepositoryFileType
{
25 fn try_from(string
: &str) -> Result
<Self, Error
> {
27 "list" => Ok(APTRepositoryFileType
::List
),
28 "sources" => Ok(APTRepositoryFileType
::Sources
),
29 _
=> bail
!("invalid file type '{}'", string
),
34 impl Display
for APTRepositoryFileType
{
35 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
37 APTRepositoryFileType
::List
=> write
!(f
, "list"),
38 APTRepositoryFileType
::Sources
=> write
!(f
, "sources"),
44 #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
45 #[serde(rename_all = "kebab-case")]
46 pub enum APTRepositoryPackageType
{
49 /// Debian source package
53 impl TryFrom
<&str> for APTRepositoryPackageType
{
56 fn try_from(string
: &str) -> Result
<Self, Error
> {
58 "deb" => Ok(APTRepositoryPackageType
::Deb
),
59 "deb-src" => Ok(APTRepositoryPackageType
::DebSrc
),
60 _
=> bail
!("invalid package type '{}'", string
),
65 impl Display
for APTRepositoryPackageType
{
66 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
68 APTRepositoryPackageType
::Deb
=> write
!(f
, "deb"),
69 APTRepositoryPackageType
::DebSrc
=> write
!(f
, "deb-src"),
77 description
: "Option key.",
81 description
: "Option values.",
84 description
: "Value.",
90 #[derive(Debug, Clone, Serialize, Deserialize)]
91 #[serde(rename_all = "PascalCase")] // for consistency
92 /// Additional options for an APT repository.
93 /// Used for both single- and mutli-value options.
94 pub struct APTRepositoryOption
{
98 pub values
: Vec
<String
>,
104 description
: "List of package types.",
107 type: APTRepositoryPackageType
,
111 description
: "List of repository URIs.",
114 description
: "Repository URI.",
119 description
: "List of distributions.",
122 description
: "Package distribution.",
127 description
: "List of repository components.",
130 description
: "Repository component.",
138 type: APTRepositoryOption
,
142 description
: "Associated comment.",
147 type: APTRepositoryFileType
,
150 description
: "Whether the repository is enabled or not.",
155 #[derive(Debug, Clone, Serialize, Deserialize)]
156 #[serde(rename_all = "PascalCase")]
157 /// Describes an APT repository.
158 pub struct APTRepository
{
159 /// List of package types.
160 #[serde(skip_serializing_if = "Vec::is_empty")]
161 pub types
: Vec
<APTRepositoryPackageType
>,
163 /// List of repository URIs.
164 #[serde(skip_serializing_if = "Vec::is_empty")]
165 #[serde(rename = "URIs")]
166 pub uris
: Vec
<String
>,
168 /// List of package distributions.
169 #[serde(skip_serializing_if = "Vec::is_empty")]
170 pub suites
: Vec
<String
>,
172 /// List of repository components.
173 #[serde(skip_serializing_if = "Vec::is_empty")]
174 pub components
: Vec
<String
>,
176 /// Additional options.
177 #[serde(skip_serializing_if = "Vec::is_empty")]
178 pub options
: Vec
<APTRepositoryOption
>,
180 /// Associated comment.
181 #[serde(skip_serializing_if = "String::is_empty")]
184 /// Format of the defining file.
185 pub file_type
: APTRepositoryFileType
,
187 /// Whether the repository is enabled or not.
192 /// Crates an empty repository.
193 pub fn new(file_type
: APTRepositoryFileType
) -> Self {
200 comment
: String
::new(),
206 /// Changes the `enabled` flag and makes sure the `Enabled` option for
207 /// `APTRepositoryPackageType::Sources` repositories is updated too.
208 pub fn set_enabled(&mut self, enabled
: bool
) {
209 self.enabled
= enabled
;
211 if self.file_type
== APTRepositoryFileType
::Sources
{
212 let enabled_string
= match enabled
{
213 true => "true".to_string(),
214 false => "false".to_string(),
216 for option
in self.options
.iter_mut() {
217 if option
.key
== "Enabled" {
218 option
.values
= vec
![enabled_string
];
222 self.options
.push(APTRepositoryOption
{
223 key
: "Enabled".to_string(),
224 values
: vec
![enabled_string
],
229 /// Makes sure that all basic properties of a repository are present and
230 /// not obviously invalid.
231 pub fn basic_check(&self) -> Result
<(), Error
> {
232 if self.types
.is_empty() {
233 bail
!("missing package type(s)");
235 if self.uris
.is_empty() {
236 bail
!("missing URI(s)");
238 if self.suites
.is_empty() {
239 bail
!("missing suite(s)");
242 for uri
in self.uris
.iter() {
243 if !uri
.contains('
:'
) || uri
.len() < 3 {
244 bail
!("invalid URI: '{}'", uri
);
248 for suite
in self.suites
.iter() {
249 if !suite
.ends_with('
/'
) && self.components
.is_empty() {
250 bail
!("missing component(s)");
251 } else if suite
.ends_with('
/'
) && !self.components
.is_empty() {
252 bail
!("absolute suite '{}' does not allow component(s)", suite
);
256 if self.file_type
== APTRepositoryFileType
::List
{
257 if self.types
.len() > 1 {
258 bail
!("more than one package type");
260 if self.uris
.len() > 1 {
261 bail
!("more than one URI");
263 if self.suites
.len() > 1 {
264 bail
!("more than one suite");
271 /// Checks if the repository is the one referenced by the handle.
272 pub fn is_referenced_repository(
274 handle
: APTRepositoryHandle
,
278 let (package_type
, handle_uris
, component
) = handle
.info(product
);
280 let mut found_uri
= false;
282 for uri
in self.uris
.iter() {
283 let uri
= uri
.trim_end_matches('
/'
);
285 found_uri
= found_uri
|| handle_uris
.iter().any(|handle_uri
| handle_uri
== uri
);
288 self.types
.contains(&package_type
)
290 // using contains would require a &String
291 && self.suites
.iter().any(|self_suite
| self_suite
== suite
)
292 && self.components
.contains(&component
)
295 /// Guess the origin from the repository's URIs.
297 /// Intended to be used as a fallback for get_cached_origin.
298 pub fn origin_from_uris(&self) -> Option
<String
> {
299 for uri
in self.uris
.iter() {
300 if let Some(host
) = host_from_uri(uri
) {
301 if host
== "proxmox.com" || host
.ends_with(".proxmox.com") {
302 return Some("Proxmox".to_string());
305 if host
== "debian.org" || host
.ends_with(".debian.org") {
306 return Some("Debian".to_string());
314 /// Get the `Origin:` value from a cached InRelease file.
315 pub fn get_cached_origin(&self) -> Result
<Option
<String
>, Error
> {
316 for uri
in self.uris
.iter() {
317 for suite
in self.suites
.iter() {
318 let file
= in_release_filename(uri
, suite
);
324 let raw
= std
::fs
::read(&file
)
325 .map_err(|err
| format_err
!("unable to read {:?} - {}", file
, err
))?
;
326 let reader
= BufReader
::new(&*raw
);
328 for line
in reader
.lines() {
330 line
.map_err(|err
| format_err
!("unable to read {:?} - {}", file
, err
))?
;
332 if let Some(value
) = line
.strip_prefix("Origin:") {
335 .trim_matches(|c
| char::is_ascii_whitespace(&c
))
346 /// Writes a repository in the corresponding format followed by a blank.
348 /// Expects that `basic_check()` for the repository was successful.
349 pub fn write(&self, w
: &mut dyn Write
) -> Result
<(), Error
> {
350 match self.file_type
{
351 APTRepositoryFileType
::List
=> write_one_line(self, w
),
352 APTRepositoryFileType
::Sources
=> write_stanza(self, w
),
357 /// Get the path to the cached InRelease file.
358 fn in_release_filename(uri
: &str, suite
: &str) -> PathBuf
{
359 let mut path
= PathBuf
::from(&crate::config
::get().dir_state
);
360 path
.push(&crate::config
::get().dir_state_lists
);
362 let filename
= uri_to_filename(uri
);
365 "{}_dists_{}_InRelease",
367 suite
.replace('
/'
, "_"), // e.g. for buster/updates
373 /// See APT's URItoFileName in contrib/strutl.cc
374 fn uri_to_filename(uri
: &str) -> String
{
375 let mut filename
= uri
;
377 if let Some(begin
) = filename
.find("://") {
378 filename
= &filename
[(begin
+ 3)..];
381 if uri
.starts_with("http://") || uri
.starts_with("https://") {
382 if let Some(begin
) = filename
.find('@'
) {
383 filename
= &filename
[(begin
+ 1)..];
387 // APT seems to only strip one final slash, so do the same
388 filename
= filename
.strip_suffix('
/'
).unwrap_or(filename
);
390 let encode_chars
= "\\|{}[]<>\"^~_=!@#$%^&*";
392 let mut encoded
= String
::with_capacity(filename
.len());
394 for b
in filename
.as_bytes().iter() {
395 if *b
<= 0x20 || *b
>= 0x7F || encode_chars
.contains(*b
as char) {
396 let mut hex
= [0u8; 2];
397 // unwrap: we're hex-encoding a single byte into a 2-byte slice
398 hex
::encode_to_slice(&[*b
], &mut hex
).unwrap();
399 let hex
= unsafe { std::str::from_utf8_unchecked(&hex) }
;
400 encoded
= format
!("{}%{}", encoded
, hex
);
402 encoded
.push(*b
as char);
406 encoded
.replace('
/'
, "_")
409 /// Get the host part from a given URI.
410 fn host_from_uri(uri
: &str) -> Option
<&str> {
411 let host
= uri
.strip_prefix("http")?
;
412 let host
= host
.strip_prefix('s'
).unwrap_or(host
);
413 let mut host
= host
.strip_prefix("://")?
;
415 if let Some(end
) = host
.find('
/'
) {
419 if let Some(begin
) = host
.find('@'
) {
420 host
= &host
[(begin
+ 1)..];
423 if let Some(end
) = host
.find('
:'
) {
430 /// Strips existing double quotes from the string first, and then adds double quotes at
431 /// the beginning and end if there is an ASCII whitespace in the `string`, which is not
433 fn quote_for_one_line(string
: &str) -> String
{
434 let mut add_quotes
= false;
435 let mut wait_for_bracket
= false;
437 // easier to just quote the whole string, so ignore pre-existing quotes
438 // currently, parsing removes them anyways, but being on the safe side is rather cheap
439 let string
= string
.replace('
"', "");
441 for c in string.chars() {
442 if wait_for_bracket {
444 wait_for_bracket = false;
449 if char::is_ascii_whitespace(&c) {
455 wait_for_bracket = true;
460 true => format!("\"{}
\"", string),
465 /// Writes a repository in one-line format followed by a blank line.
467 /// Expects that `repo.file_type == APTRepositoryFileType::List`.
469 /// Expects that `basic_check()` for the repository was successful.
470 fn write_one_line(repo: &APTRepository, w: &mut dyn Write) -> Result<(), Error> {
471 if repo.file_type != APTRepositoryFileType::List {
472 bail!("not a
.list repository
");
475 if !repo.comment.is_empty() {
476 for line in repo.comment.lines() {
477 writeln!(w, "#{}", line)?;
485 write!(w, "{} ", repo.types[0])?;
487 if !repo.options.is_empty() {
490 for option in repo.options.iter() {
491 let option = quote_for_one_line(&format!("{}={}", option.key, option.values.join(",")));
492 write!(w, "{} ", option)?;
498 write!(w, "{} ", quote_for_one_line(&repo.uris[0]))?;
499 write!(w, "{} ", quote_for_one_line(&repo.suites[0]))?;
505 .map(|comp| quote_for_one_line(comp))
506 .collect::<Vec<String>>()
515 /// Writes a single stanza followed by a blank line.
517 /// Expects that `repo.file_type == APTRepositoryFileType::Sources`.
518 fn write_stanza(repo: &APTRepository, w: &mut dyn Write) -> Result<(), Error> {
519 if repo.file_type != APTRepositoryFileType::Sources {
520 bail!("not a .sources repository");
523 if !repo.comment.is_empty() {
524 for line in repo.comment.lines() {
525 writeln!(w, "#{}", line)?;
529 write!(w, "Types:")?;
532 .try_for_each(|package_type| write!(w, " {}", package_type))?;
535 writeln!(w, "URIs: {}", repo.uris.join(" "))?;
536 writeln!(w, "Suites: {}", repo.suites.join(" "))?;
538 if !repo.components.is_empty() {
539 writeln!(w, "Components: {}", repo.components.join(" "))?;
542 for option in repo.options.iter() {
543 writeln!(w, "{}: {}", option.key, option.values.join(" "))?;
552 fn test_uri_to_filename() {
553 let filename = uri_to_filename("https://some_host/some/path");
554 assert_eq
!(filename
, "some%5fhost_some_path".to_string());