Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/pypi_mapping/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ version = "0.1.0"

[dependencies]
async-once-cell = { workspace = true }
dashmap = { workspace = true }
fs-err = { workspace = true }
futures = { workspace = true }
http-cache-reqwest = { workspace = true }
Expand All @@ -27,5 +28,6 @@ reqwest-middleware = { workspace = true }
reqwest-retry = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
url = { workspace = true }
182 changes: 182 additions & 0 deletions crates/pypi_mapping/src/custom_mapping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
use std::path::Path;

use async_once_cell::OnceCell as AsyncCell;
use miette::{IntoDiagnostic, WrapErr};
use rattler_conda_types::{PackageUrl, RepoDataRecord};
use reqwest_middleware::ClientWithMiddleware;
use url::Url;

use crate::{
CompressedMapping, DerivePurls, MappingByChannel, MappingError, MappingLocation, MappingMap,
PurlSource,
};

/// Struct with a mapping of channel names to their respective mapping locations
/// location could be a remote url or local file.
///
/// This struct caches the mapping internally.
#[derive(Debug)]
pub struct CustomMapping {
pub mapping: MappingMap,
mapping_value: AsyncCell<MappingByChannel>,
}

impl CustomMapping {
/// Create a new `CustomMapping` with the specified mapping.
pub fn new(mapping: MappingMap) -> Self {
Self {
mapping,
mapping_value: Default::default(),
}
}

/// Fetch the custom mapping from the server or load from the local
pub async fn fetch_custom_mapping(
&self,
client: &ClientWithMiddleware,
) -> miette::Result<MappingByChannel> {
self.mapping_value
.get_or_try_init(async {
let mut mapping_url_to_name: MappingByChannel = Default::default();

for (name, url) in self.mapping.iter() {
// Fetch the mapping from the server or from the local

match url {
MappingLocation::Url(url) => {
let mapping_by_name = match url.scheme() {
"file" => {
let file_path = url.to_file_path().map_err(|_| {
miette::miette!("{} is not a valid file url", url)
})?;
fetch_mapping_from_path(&file_path)?
}
_ => fetch_mapping_from_url(client, url).await?,
};

mapping_url_to_name.insert(name.to_string(), mapping_by_name);
}
MappingLocation::Path(path) => {
let mapping_by_name = fetch_mapping_from_path(path)?;

mapping_url_to_name.insert(name.to_string(), mapping_by_name);
}
MappingLocation::Memory(mapping) => {
mapping_url_to_name.insert(name.to_string(), mapping.clone());
}
}
}

Ok(mapping_url_to_name)
})
.await
.cloned()
}
}

async fn fetch_mapping_from_url(
client: &ClientWithMiddleware,
url: &Url,
) -> miette::Result<CompressedMapping> {
let response = client
.get(url.clone())
.send()
.await
.into_diagnostic()
.context(format!(
"failed to download pypi mapping from {} location",
url.as_str()
))?;

if !response.status().is_success() {
return Err(miette::miette!(
"Could not request mapping located at {:?}",
url.as_str()
));
}

let mapping_by_name = response.json().await.into_diagnostic().context(format!(
"failed to parse pypi name mapping located at {}. Please make sure that it's a valid json",
url
))?;

Ok(mapping_by_name)
}

fn fetch_mapping_from_path(path: &Path) -> miette::Result<CompressedMapping> {
let file = fs_err::File::open(path)
.into_diagnostic()
.context(format!("failed to open file {}", path.display()))?;
let reader = std::io::BufReader::new(file);
let mapping_by_name = serde_json::from_reader(reader)
.into_diagnostic()
.context(format!(
"failed to parse pypi name mapping located at {}. Please make sure that it's a valid json",
path.display()
))?;

Ok(mapping_by_name)
}

/// THis is a client that uses a custom in memory mapping to derive purls.
#[derive(Default)]
pub(crate) struct CustomMappingClient {
mapping: MappingByChannel,
}

impl CustomMappingClient {
/// Returns the mapping associated with a channel.
fn get_channel_mapping(&self, channel: &str) -> Option<&CompressedMapping> {
self.mapping.get(channel.trim_end_matches('/'))
}

/// Returns true if this mapping applies to the given record.
pub fn is_mapping_for_record(&self, record: &RepoDataRecord) -> bool {
record
.channel
.as_ref()
.is_some_and(|channel| self.get_channel_mapping(channel).is_some())
}
}

impl From<MappingByChannel> for CustomMappingClient {
fn from(value: MappingByChannel) -> Self {
Self { mapping: value }
}
}

impl DerivePurls for CustomMappingClient {
async fn derive_purls(
&self,
record: &RepoDataRecord,
) -> Result<Option<Vec<PackageUrl>>, MappingError> {
let Some(channel) = record.channel.as_ref() else {
return Ok(None);
};

// See if the mapping contains the channel
let Some(custom_mapping) = self.get_channel_mapping(channel) else {
return Ok(None);
};

// Find the mapping for this particular record
match custom_mapping.get(record.package_record.name.as_normalized()) {
// The record is in the mapping, and it has a pypi name
Some(Some(mapped_name)) => {
let purl = PackageUrl::builder(String::from("pypi"), mapped_name.to_string())
.with_qualifier("source", PurlSource::ProjectDefinedMapping.as_str())
.expect("valid qualifier");
let built_purl = purl.build().expect("valid pypi package url");
Ok(Some(vec![built_purl]))
}
Some(None) => {
// The record is in the mapping, but it has no pypi name
Ok(Some(vec![]))
}
None => {
// The record is not in the mapping
Ok(None)
}
}
}
}
Loading