This commit is contained in:
@@ -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(¤t_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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user