format: run cargo fmt
All checks were successful
CI / build (push) Successful in 1m44s

This commit is contained in:
2025-11-30 12:51:52 +01:00
parent c813823a1a
commit 2cfbb69fe7
5 changed files with 491 additions and 232 deletions

View File

@@ -1,12 +1,12 @@
use chrono::NaiveDate;
use flate2::read::GzDecoder;
use std::io::Read;
use std::collections::HashMap;
use std::error::Error;
use std::io::Read;
use std::path::Path;
use chrono::NaiveDate;
use log::{debug, warn};
use crate::ProgressCallback;
use log::{debug, warn};
const BASE_URL_UBUNTU: &str = "http://archive.ubuntu.com/ubuntu";
const BASE_URL_DEBIAN: &str = "http://deb.debian.org/debian";
@@ -17,7 +17,7 @@ async fn check_launchpad_repo(package: &str) -> Result<Option<String>, Box<dyn E
.redirect(reqwest::redirect::Policy::none())
.build()?;
let response = client.head(&url).send().await?;
if response.status().is_success() {
Ok(Some(url))
} else {
@@ -29,11 +29,17 @@ fn parse_series_csv(content: &str) -> Result<Vec<String>, Box<dyn Error>> {
let mut rdr = csv::ReaderBuilder::new()
.flexible(true)
.from_reader(content.as_bytes());
let headers = rdr.headers()?.clone();
let series_idx = headers.iter().position(|h| h == "series").ok_or("Column 'series' not found")?;
let created_idx = headers.iter().position(|h| h == "created").ok_or("Column 'created' not found")?;
let series_idx = headers
.iter()
.position(|h| h == "series")
.ok_or("Column 'series' not found")?;
let created_idx = headers
.iter()
.position(|h| h == "created")
.ok_or("Column 'created' not found")?;
let mut entries = Vec::new();
for result in rdr.records() {
let record = result?;
@@ -43,10 +49,10 @@ fn parse_series_csv(content: &str) -> Result<Vec<String>, Box<dyn Error>> {
}
}
}
// Sort by date descending (newest first)
entries.sort_by(|a, b| b.1.cmp(&a.1));
Ok(entries.into_iter().map(|(s, _)| s).collect())
}
@@ -54,11 +60,17 @@ async fn get_ordered_series(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
let content = if Path::new(format!("/usr/share/distro-info/{dist}.csv").as_str()).exists() {
std::fs::read_to_string(format!("/usr/share/distro-info/{dist}.csv"))?
} else {
reqwest::get(format!("https://salsa.debian.org/debian/distro-info-data/-/raw/main/{dist}.csv").as_str()).await?.text().await?
reqwest::get(
format!("https://salsa.debian.org/debian/distro-info-data/-/raw/main/{dist}.csv")
.as_str(),
)
.await?
.text()
.await?
};
let mut series = parse_series_csv(&content)?;
// For Debian, ensure 'sid' is first if it's not (it usually doesn't have a date or is very old/new depending on file)
// Actually in the file sid has 1993 date.
// But we want to try 'sid' (unstable) first for Debian.
@@ -66,7 +78,7 @@ async fn get_ordered_series(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
series.retain(|s| s != "sid");
series.insert(0, "sid".to_string());
}
Ok(series)
}
@@ -85,7 +97,11 @@ pub async fn get_dist_series(dist: &str) -> Result<Vec<String>, Box<dyn Error>>
if Path::new(format!("/usr/share/distro-info/{dist}.csv").as_str()).exists() {
get_series_from_file(format!("/usr/share/distro-info/{dist}.csv").as_str())
} else {
get_series_from_url(format!("https://salsa.debian.org/debian/distro-info-data/-/raw/main/{dist}.csv").as_str()).await
get_series_from_url(
format!("https://salsa.debian.org/debian/distro-info-data/-/raw/main/{dist}.csv")
.as_str(),
)
.await
}
}
@@ -118,18 +134,21 @@ pub struct PackageStanza {
pub files: Vec<FileEntry>,
}
#[derive(Debug)]
pub struct PackageInfo {
pub dist: String,
pub series: String,
pub stanza: PackageStanza,
pub preferred_vcs: Option<String>,
pub archive_url: String
pub archive_url: String,
}
fn get_sources_url(base_url: &str, series: &str, pocket: &str, component: &str) -> String {
let pocket_full = if pocket.is_empty() { String::new() } else { format!("-{}", pocket) };
let pocket_full = if pocket.is_empty() {
String::new()
} else {
format!("-{}", pocket)
};
format!("{base_url}/dists/{series}{pocket_full}/{component}/source/Sources.gz")
}
@@ -145,27 +164,38 @@ fn get_base_url(dist: &str) -> &str {
* Obtain the URL for the 'Release' file of a distribution series
*/
fn get_release_url(base_url: &str, series: &str, pocket: &str) -> String {
let pocket_full = if pocket.is_empty() { String::new() } else { format!("-{}", pocket) };
let pocket_full = if pocket.is_empty() {
String::new()
} else {
format!("-{}", pocket)
};
format!("{base_url}/dists/{series}{pocket_full}/Release")
}
/*
* Obtain the components of a distribution series by parsing the 'Release' file
*/
async fn get_components(base_url: &str, series: &str, pocket: &str) -> Result<Vec<String>, Box<dyn Error>> {
async fn get_components(
base_url: &str,
series: &str,
pocket: &str,
) -> Result<Vec<String>, Box<dyn Error>> {
let url = get_release_url(base_url, series, pocket);
debug!("Fetching Release file from: {}", url);
let content = reqwest::get(&url).await?.text().await?;
for line in content.lines() {
if line.starts_with("Components:") {
if let Some((_, components)) = line.split_once(':') {
return Ok(components.split_whitespace().map(|s| s.to_string()).collect());
return Ok(components
.split_whitespace()
.map(|s| s.to_string())
.collect());
}
}
}
Err("Components not found.".into())
}
@@ -173,7 +203,11 @@ async fn get_components(base_url: &str, series: &str, pocket: &str) -> Result<Ve
* Parse a 'Sources.gz' debian package file data, to look for a target package and
* return the data for that package stanza
*/
fn parse_sources(data: &[u8], target_package: &str, target_version: Option<&str>) -> Result<Option<PackageStanza>, Box<dyn Error>> {
fn parse_sources(
data: &[u8],
target_package: &str,
target_version: Option<&str>,
) -> Result<Option<PackageStanza>, Box<dyn Error>> {
let mut d = GzDecoder::new(data);
let mut s = String::new();
d.read_to_string(&mut s)?;
@@ -181,10 +215,12 @@ fn parse_sources(data: &[u8], target_package: &str, target_version: Option<&str>
for stanza in s.split("\n\n") {
let mut fields: HashMap<String, String> = HashMap::new();
let mut current_key = String::new();
for line in stanza.lines() {
if line.is_empty() { continue; }
if line.is_empty() {
continue;
}
if line.starts_with(' ') || line.starts_with('\t') {
// Continuation line
if let Some(val) = fields.get_mut(&current_key) {
@@ -235,11 +271,16 @@ fn parse_sources(data: &[u8], target_package: &str, target_version: Option<&str>
}
}
}
Ok(None)
}
pub async fn get(package_name: &str, series: &str, pocket: &str, version: Option<&str>) -> Result<PackageInfo, Box<dyn Error>> {
pub async fn get(
package_name: &str,
series: &str,
pocket: &str,
version: Option<&str>,
) -> Result<PackageInfo, Box<dyn Error>> {
let dist = get_dist_from_series(series).await?;
// Handle Ubuntu case: Vcs-Git does not usually point to Launchpad but Salsa
@@ -261,7 +302,7 @@ pub async fn get(package_name: &str, series: &str, pocket: &str, version: Option
let url = get_sources_url(base_url, series, pocket, &component);
debug!("Fetching sources from: {}", url);
let response = match reqwest::get(&url).await {
Ok(resp) => resp,
Err(e) => {
@@ -269,16 +310,19 @@ pub async fn get(package_name: &str, series: &str, pocket: &str, version: Option
continue;
}
};
if !response.status().is_success() {
debug!("Failed to fetch {}: status {}", url, response.status());
continue;
}
let compressed_data = response.bytes().await?;
debug!("Downloaded Sources.gz for {}/{}/{}", dist, series, component);
debug!(
"Downloaded Sources.gz for {}/{}/{}",
dist, series, component
);
if let Some(stanza) = parse_sources(&compressed_data, package_name, version)? {
if let Some(vcs) = &stanza.vcs_git {
if preferred_vcs.is_none() {
@@ -296,13 +340,23 @@ pub async fn get(package_name: &str, series: &str, pocket: &str, version: Option
});
}
}
Err(format!("Package '{}' not found in {}/{}", package_name, dist, series).into())
Err(format!(
"Package '{}' not found in {}/{}",
package_name, dist, series
)
.into())
}
pub async fn find_package(package_name: &str, dist: &str, pocket: &str, version: Option<&str>, progress: ProgressCallback<'_>) -> Result<PackageInfo, Box<dyn Error>> {
pub async fn find_package(
package_name: &str,
dist: &str,
pocket: &str,
version: Option<&str>,
progress: ProgressCallback<'_>,
) -> Result<PackageInfo, Box<dyn Error>> {
let series_list = get_ordered_series(dist).await?;
for (i, series) in series_list.iter().enumerate() {
if let Some(cb) = progress {
cb("", &format!("Checking {}...", series), i, series_list.len());
@@ -311,18 +365,21 @@ pub async fn find_package(package_name: &str, dist: &str, pocket: &str, version:
match get(package_name, series, pocket, version).await {
Ok(info) => {
if i > 0 {
warn!("Package '{}' not found in development release. Found in {}/{}.", package_name, dist, series);
warn!(
"Package '{}' not found in development release. Found in {}/{}.",
package_name, dist, series
);
} else {
debug!("Found package '{}' in {}/{}", package_name, dist, series);
}
return Ok(info);
},
}
Err(_e) => {
continue;
}
}
}
Err(format!("Package '{}' not found.", package_name).into())
}
@@ -335,10 +392,15 @@ mod tests {
// "hello" should exist on Launchpad for Ubuntu
let url = check_launchpad_repo("hello").await.unwrap();
assert!(url.is_some());
assert_eq!(url.unwrap(), "https://git.launchpad.net/ubuntu/+source/hello");
assert_eq!(
url.unwrap(),
"https://git.launchpad.net/ubuntu/+source/hello"
);
// "this-package-should-not-exist-12345" should not exist
let url = check_launchpad_repo("this-package-should-not-exist-12345").await.unwrap();
let url = check_launchpad_repo("this-package-should-not-exist-12345")
.await
.unwrap();
assert!(url.is_none());
}
@@ -355,7 +417,7 @@ mod tests {
assert!(series.contains(&"noble".to_string()));
assert!(series.contains(&"jammy".to_string()));
}
#[tokio::test]
async fn test_get_dist_from_series() {
assert_eq!(get_dist_from_series("sid").await.unwrap(), "debian");
@@ -364,12 +426,12 @@ mod tests {
#[test]
fn test_parse_sources() {
use flate2::write::GzEncoder;
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
let data = "Package: hello\nVersion: 2.10-2\nDirectory: pool/main/h/hello\nVcs-Git: https://salsa.debian.org/debian/hello.git\n\nPackage: other\nVersion: 1.0\n";
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(data.as_bytes()).unwrap();
let compressed = encoder.finish().unwrap();
@@ -378,7 +440,10 @@ mod tests {
assert_eq!(info.package, "hello");
assert_eq!(info.version, "2.10-2");
assert_eq!(info.directory, "pool/main/h/hello");
assert_eq!(info.vcs_git.unwrap(), "https://salsa.debian.org/debian/hello.git");
assert_eq!(
info.vcs_git.unwrap(),
"https://salsa.debian.org/debian/hello.git"
);
let none = parse_sources(&compressed, "missing", None).unwrap();
assert!(none.is_none());
@@ -387,7 +452,9 @@ mod tests {
#[tokio::test]
async fn test_find_package_fallback() {
// python2.7 is in bullseye but not above
let info = find_package("python2.7", "debian", "", None, None).await.unwrap();
let info = find_package("python2.7", "debian", "", None, None)
.await
.unwrap();
assert_eq!(info.stanza.package, "python2.7");
assert_eq!(info.series, "bullseye")
}
@@ -395,7 +462,9 @@ mod tests {
#[tokio::test]
async fn test_find_package_devel() {
// hello is in sid
let info = find_package("hello", "debian", "", None, None).await.unwrap();
let info = find_package("hello", "debian", "", None, None)
.await
.unwrap();
assert_eq!(info.stanza.package, "hello");
assert_eq!(info.series, "sid")
}