use crate::{Certainty, UpstreamDatum, UpstreamDatumWithMetadata};
use crate::{ProviderError, UpstreamMetadata};
use log::warn;
const DEFAULT_ITERATION_LIMIT: usize = 10;
type ExtrapolationCallback = fn(
UpstreamMetadata,
bool,
) -> std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<Vec<UpstreamDatumWithMetadata>, ProviderError>>
+ Send,
>,
>;
struct Extrapolation {
from_fields: &'static [&'static str],
to_fields: &'static [&'static str],
cb: ExtrapolationCallback,
}
async fn extrapolate_repository_from_homepage(
upstream_metadata: &UpstreamMetadata,
net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let mut ret = vec![];
let homepage = upstream_metadata.get("Homepage").unwrap();
let url = match homepage.datum.to_url() {
Some(url) => url,
None => {
return {
warn!("Homepage field is not a URL");
Ok(vec![])
}
}
};
if let Some(repo) = crate::vcs::guess_repo_from_url(&url, Some(net_access)).await {
ret.push(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Repository(repo),
certainty: Some(
std::cmp::min(homepage.certainty, Some(Certainty::Likely))
.unwrap_or(Certainty::Likely),
),
origin: homepage.origin.clone(),
});
}
Ok(ret)
}
async fn extrapolate_homepage_from_repository_browse(
upstream_metadata: &UpstreamMetadata,
net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let mut ret = vec![];
let browse_url = upstream_metadata.get("Repository-Browse").unwrap();
let url = match browse_url.datum.to_url() {
Some(url) => url,
None => {
return {
warn!("Repository-Browse field is not a URL");
Ok(vec![])
}
}
};
let forge = crate::find_forge(&url, Some(net_access)).await;
if forge.is_some() && forge.unwrap().repository_browse_can_be_homepage() {
ret.push(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Homepage(browse_url.datum.as_str().unwrap().to_string()),
certainty: Some(
std::cmp::min(browse_url.certainty, Some(Certainty::Possible))
.unwrap_or(Certainty::Possible),
),
origin: browse_url.origin.clone(),
});
}
Ok(ret)
}
async fn copy_bug_db_field(
upstream_metadata: &UpstreamMetadata,
_net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let mut ret = vec![];
let old_bug_db = upstream_metadata.get("Bugs-Database").unwrap();
ret.push(UpstreamDatumWithMetadata {
datum: UpstreamDatum::BugDatabase(old_bug_db.datum.as_str().unwrap().to_string()),
certainty: old_bug_db.certainty,
origin: old_bug_db.origin.clone(),
});
Ok(ret)
}
async fn extrapolate_repository_from_bug_db(
upstream_metadata: &UpstreamMetadata,
net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let old_value = upstream_metadata.get("Bug-Database").unwrap();
let url = match old_value.datum.to_url() {
Some(url) => url,
None => {
return {
warn!("Bug-Database field is not a URL");
Ok(vec![])
}
}
};
let repo = crate::vcs::guess_repo_from_url(&url, Some(net_access)).await;
Ok(if let Some(repo) = repo {
vec![UpstreamDatumWithMetadata {
datum: UpstreamDatum::Repository(repo),
certainty: Some(
std::cmp::min(old_value.certainty, Some(Certainty::Likely))
.unwrap_or(Certainty::Likely),
),
origin: old_value.origin.clone(),
}]
} else {
vec![]
})
}
async fn extrapolate_repository_browse_from_repository(
upstream_metadata: &UpstreamMetadata,
net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let old_value = upstream_metadata.get("Repository").unwrap();
let url = match old_value.datum.to_url() {
Some(url) => url,
None => {
return {
warn!("Repository field is not a URL");
Ok(vec![])
}
}
};
let browse_url = crate::vcs::browse_url_from_repo_url(
&crate::vcs::VcsLocation {
url,
branch: None,
subpath: None,
},
Some(net_access),
)
.await;
Ok(if let Some(browse_url) = browse_url {
vec![UpstreamDatumWithMetadata {
datum: UpstreamDatum::RepositoryBrowse(browse_url.to_string()),
certainty: old_value.certainty,
origin: old_value.origin.clone(),
}]
} else {
vec![]
})
}
async fn extrapolate_repository_from_repository_browse(
upstream_metadata: &UpstreamMetadata,
net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let old_value = upstream_metadata.get("Repository-Browse").unwrap();
let url = match old_value.datum.to_url() {
Some(url) => url,
None => {
return {
warn!("Repository-Browse field is not a URL");
Ok(vec![])
}
}
};
let repo = crate::vcs::guess_repo_from_url(&url, Some(net_access)).await;
Ok(if let Some(repo) = repo {
vec![UpstreamDatumWithMetadata {
datum: UpstreamDatum::Repository(repo),
certainty: old_value.certainty,
origin: old_value.origin.clone(),
}]
} else {
vec![]
})
}
async fn extrapolate_bug_database_from_repository(
upstream_metadata: &UpstreamMetadata,
net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let old_value = upstream_metadata.get("Repository").unwrap();
let url = match old_value.datum.to_url() {
Some(url) => url,
None => {
return {
warn!("Repository field is not a URL");
Ok(vec![])
}
}
};
Ok(
if let Some(bug_db_url) =
crate::guess_bug_database_url_from_repo_url(&url, Some(net_access)).await
{
vec![UpstreamDatumWithMetadata {
datum: UpstreamDatum::BugDatabase(bug_db_url.to_string()),
certainty: Some(
std::cmp::min(old_value.certainty, Some(Certainty::Likely))
.unwrap_or(Certainty::Likely),
),
origin: old_value.origin.clone(),
}]
} else {
vec![]
},
)
}
async fn extrapolate_bug_submit_from_bug_db(
upstream_metadata: &UpstreamMetadata,
net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let old_value = upstream_metadata.get("Bug-Database").unwrap();
let url = match old_value.datum.to_url() {
Some(url) => url,
None => {
return {
warn!("Bug-Database field is not a URL");
Ok(vec![])
}
}
};
let bug_submit_url = crate::bug_submit_url_from_bug_database_url(&url, Some(net_access)).await;
Ok(if let Some(bug_submit_url) = bug_submit_url {
vec![UpstreamDatumWithMetadata {
datum: UpstreamDatum::BugSubmit(bug_submit_url.to_string()),
certainty: old_value.certainty,
origin: old_value.origin.clone(),
}]
} else {
vec![]
})
}
async fn extrapolate_bug_db_from_bug_submit(
upstream_metadata: &UpstreamMetadata,
net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let old_value = upstream_metadata.get("Bug-Submit").unwrap();
let old_value_url = match old_value.datum.to_url() {
Some(url) => url,
None => return Ok(vec![]),
};
let bug_db_url =
crate::bug_database_url_from_bug_submit_url(&old_value_url, Some(net_access)).await;
Ok(if let Some(bug_db_url) = bug_db_url {
vec![UpstreamDatumWithMetadata {
datum: UpstreamDatum::BugDatabase(bug_db_url.to_string()),
certainty: old_value.certainty,
origin: old_value.origin.clone(),
}]
} else {
vec![]
})
}
async fn extrapolate_repository_from_download(
upstream_metadata: &UpstreamMetadata,
net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let old_value = upstream_metadata.get("Download").unwrap();
let url = match old_value.datum.to_url() {
Some(url) => url,
None => {
return {
warn!("Download field is not a URL");
Ok(vec![])
}
}
};
let repo = crate::vcs::guess_repo_from_url(&url, Some(net_access)).await;
Ok(if let Some(repo) = repo {
vec![UpstreamDatumWithMetadata {
datum: UpstreamDatum::Repository(repo),
certainty: Some(
std::cmp::min(old_value.certainty, Some(Certainty::Likely))
.unwrap_or(Certainty::Likely),
),
origin: old_value.origin.clone(),
}]
} else {
vec![]
})
}
async fn extrapolate_name_from_repository(
upstream_metadata: &UpstreamMetadata,
net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let mut ret = vec![];
let old_value = upstream_metadata.get("Repository").unwrap();
let url = match old_value.datum.to_url() {
Some(url) => url,
None => {
return {
warn!("Repository field is not a URL");
Ok(vec![])
}
}
};
let repo = crate::vcs::guess_repo_from_url(&url, Some(net_access)).await;
if let Some(repo) = repo {
let parsed: url::Url = repo.parse().unwrap();
let name = parsed.path_segments().unwrap().next_back().unwrap();
let name = name.strip_suffix(".git").unwrap_or(name);
if !name.is_empty() {
ret.push(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Name(name.to_string()),
certainty: Some(
std::cmp::min(old_value.certainty, Some(Certainty::Likely))
.unwrap_or(Certainty::Likely),
),
origin: old_value.origin.clone(),
});
}
}
Ok(ret)
}
async fn extrapolate_security_contact_from_security_md(
upstream_metadata: &UpstreamMetadata,
net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let repository_url = upstream_metadata.get("Repository").unwrap();
let security_md_path = upstream_metadata.get("Security-MD").unwrap();
let url = match repository_url.datum.to_url() {
Some(url) => url,
None => {
return {
warn!("Repository field is not a URL");
Ok(vec![])
}
}
};
let security_url = crate::vcs::browse_url_from_repo_url(
&crate::vcs::VcsLocation {
url,
branch: None,
subpath: security_md_path.datum.as_str().map(|x| x.to_string()),
},
Some(net_access),
)
.await;
Ok(if let Some(security_url) = security_url {
vec![UpstreamDatumWithMetadata {
datum: UpstreamDatum::SecurityContact(security_url.to_string()),
certainty: std::cmp::min(repository_url.certainty, security_md_path.certainty),
origin: repository_url.origin.clone(),
}]
} else {
vec![]
})
}
async fn extrapolate_contact_from_maintainer(
upstream_metadata: &UpstreamMetadata,
_net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let maintainer = upstream_metadata.get("Maintainer").unwrap();
Ok(vec![UpstreamDatumWithMetadata {
datum: UpstreamDatum::Contact(maintainer.datum.as_person().unwrap().to_string()),
certainty: maintainer.certainty,
origin: maintainer.origin.clone(),
}])
}
async fn consult_homepage(
upstream_metadata: &UpstreamMetadata,
net_access: bool,
) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
if !net_access {
return Ok(vec![]);
}
let homepage = upstream_metadata.get("Homepage").unwrap();
let url = match homepage.datum.to_url() {
Some(url) => url,
None => {
return {
warn!("Homepage field is not a URL");
Ok(vec![])
}
}
};
let mut ret = vec![];
for mut entry in crate::homepage::guess_from_homepage(&url).await? {
entry.certainty = std::cmp::min(homepage.certainty, entry.certainty);
ret.push(entry);
}
Ok(ret)
}
const EXTRAPOLATIONS: &[Extrapolation] = &[
Extrapolation {
from_fields: &["Homepage"],
to_fields: &["Repository"],
cb: |us, na| Box::pin(async move { extrapolate_repository_from_homepage(&us, na).await }),
},
Extrapolation {
from_fields: &["Repository-Browse"],
to_fields: &["Homepage"],
cb: |us, na| {
Box::pin(async move { extrapolate_homepage_from_repository_browse(&us, na).await })
},
},
Extrapolation {
from_fields: &["Bugs-Database"],
to_fields: &["Bug-Database"],
cb: |us, na| Box::pin(async move { copy_bug_db_field(&us, na).await }),
},
Extrapolation {
from_fields: &["Bug-Database"],
to_fields: &["Repository"],
cb: |us, na| Box::pin(async move { extrapolate_repository_from_bug_db(&us, na).await }),
},
Extrapolation {
from_fields: &["Repository"],
to_fields: &["Repository-Browse"],
cb: |us, na| {
Box::pin(async move { extrapolate_repository_browse_from_repository(&us, na).await })
},
},
Extrapolation {
from_fields: &["Repository-Browse"],
to_fields: &["Repository"],
cb: |us, na| {
Box::pin(async move { extrapolate_repository_from_repository_browse(&us, na).await })
},
},
Extrapolation {
from_fields: &["Repository"],
to_fields: &["Bug-Database"],
cb: |us, na| {
Box::pin(async move { extrapolate_bug_database_from_repository(&us, na).await })
},
},
Extrapolation {
from_fields: &["Bug-Database"],
to_fields: &["Bug-Submit"],
cb: |us, na| Box::pin(async move { extrapolate_bug_submit_from_bug_db(&us, na).await }),
},
Extrapolation {
from_fields: &["Bug-Submit"],
to_fields: &["Bug-Database"],
cb: |us, na| Box::pin(async move { extrapolate_bug_db_from_bug_submit(&us, na).await }),
},
Extrapolation {
from_fields: &["Download"],
to_fields: &["Repository"],
cb: |us, na| Box::pin(async move { extrapolate_repository_from_download(&us, na).await }),
},
Extrapolation {
from_fields: &["Repository"],
to_fields: &["Name"],
cb: |us, na| Box::pin(async move { extrapolate_name_from_repository(&us, na).await }),
},
Extrapolation {
from_fields: &["Repository", "Security-MD"],
to_fields: &["Security-Contact"],
cb: |us, na| {
Box::pin(async move { extrapolate_security_contact_from_security_md(&us, na).await })
},
},
Extrapolation {
from_fields: &["Maintainer"],
to_fields: &["Contact"],
cb: |us, na| Box::pin(async move { extrapolate_contact_from_maintainer(&us, na).await }),
},
Extrapolation {
from_fields: &["Homepage"],
to_fields: &["Bug-Database", "Repository"],
cb: |us, na| Box::pin(async move { consult_homepage(&us, na).await }),
},
];
pub async fn extrapolate_fields(
upstream_metadata: &mut UpstreamMetadata,
net_access: bool,
iteration_limit: Option<usize>,
) -> Result<(), ProviderError> {
let iteration_limit = iteration_limit.unwrap_or(DEFAULT_ITERATION_LIMIT);
let mut changed = true;
let mut iterations = 0;
while changed {
changed = false;
iterations += 1;
if iterations > iteration_limit {
return Err(ProviderError::ExtrapolationLimitExceeded(iteration_limit));
}
for extrapolation in EXTRAPOLATIONS {
let from_fields = extrapolation.from_fields;
let to_fields = extrapolation.to_fields;
let cb = extrapolation.cb;
let from_values = from_fields
.iter()
.map(|f| upstream_metadata.get(f))
.collect::<Vec<_>>();
if !from_values.iter().all(|v| v.is_some()) {
log::trace!(
"Not enough values for extrapolation from {:?} to {:?}",
from_fields,
to_fields
);
continue;
}
let from_values = from_values
.iter()
.map(|v| v.unwrap().clone())
.collect::<Vec<_>>();
let from_certainties = from_fields
.iter()
.map(|f| upstream_metadata.get(f).unwrap().certainty)
.collect::<Vec<_>>();
let from_certainty = *from_certainties.iter().min().unwrap();
let old_to_values: std::collections::HashMap<_, _> = to_fields
.iter()
.filter_map(|f| upstream_metadata.get(f).map(|v| (f, v.clone())))
.collect();
assert!(old_to_values.values().all(|v| v.certainty.is_some()));
if to_fields.iter().all(|f| {
old_to_values
.get(f)
.map(|v| v.certainty >= from_certainty)
.unwrap_or(false)
}) {
log::trace!(
"Not extrapolating from {:?} to {:?} because of certainty ({:?} >= {:?})",
from_fields,
to_fields,
old_to_values
.values()
.map(|v| v.certainty)
.collect::<Vec<_>>(),
from_certainty
);
continue;
}
let extra_upstream_metadata = cb(upstream_metadata.clone(), net_access).await?;
let changes = upstream_metadata.update(extra_upstream_metadata.into_iter());
if !changes.is_empty() {
log::debug!(
"Extrapolating ({:?} ⇒ {:?}) from ({:?})",
old_to_values
.iter()
.map(|(k, v)| format!("{}: {}", k, v.datum))
.collect::<Vec<_>>(),
changes
.iter()
.map(|d| format!("{}: {}", d.datum.field(), d.datum))
.collect::<Vec<_>>(),
from_values
.iter()
.map(|v| format!(
"{}: {} ({})",
v.datum.field(),
v.datum,
v.certainty
.map_or_else(|| "unknown".to_string(), |c| c.to_string())
))
.collect::<Vec<_>>()
);
changed = true;
}
}
}
Ok(())
}