Skip to content

Commit

Permalink
Introduce a hash policy abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Apr 10, 2024
1 parent da47a09 commit 446ffd2
Show file tree
Hide file tree
Showing 27 changed files with 511 additions and 458 deletions.
80 changes: 67 additions & 13 deletions crates/distribution-types/src/hashed.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,81 @@
use pypi_types::HashDigest;
use pypi_types::{HashAlgorithm, HashDigest};

pub trait Hashed {
/// Return the [`HashDigest`]s for the archive.
fn hashes(&self) -> &[HashDigest];

/// Returns `true` if the archive satisfies the given hashes.
fn satisfies(&self, hashes: &[HashDigest]) -> bool {
if hashes.is_empty() {
true
} else {
self.hashes().iter().any(|hash| hashes.contains(hash))
/// Returns `true` if the archive satisfies the given hash policy.
fn satisfies(&self, hashes: HashPolicy) -> bool {
match hashes {
HashPolicy::None => true,
HashPolicy::Generate => true,
HashPolicy::Validate(hashes) => self.hashes().iter().any(|hash| hashes.contains(hash)),
}
}

/// Returns `true` if the archive includes a hash for at least one of the given algorithms.
fn has_digests(&self, hashes: &[HashDigest]) -> bool {
if hashes.is_empty() {
true
} else {
hashes
fn has_digests(&self, hashes: HashPolicy) -> bool {
match hashes {
HashPolicy::None => true,
HashPolicy::Generate => self
.hashes()
.iter()
.any(|hash| hash.algorithm == HashAlgorithm::Sha256),
HashPolicy::Validate(hashes) => hashes
.iter()
.map(HashDigest::algorithm)
.any(|algorithm| self.hashes().iter().any(|hash| hash.algorithm == algorithm))
.any(|algorithm| self.hashes().iter().any(|hash| hash.algorithm == algorithm)),
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashPolicy<'a> {
/// No hash policy is specified.
None,
/// Hashes should be generated (specifically, a SHA-256 hash), but not validated.
Generate,
/// Hashes should be validated against a pre-defined list of hashes. If necessary, hashes should
/// be generated so as to ensure that the archive is valid.
Validate(&'a [HashDigest]),
}

impl<'a> HashPolicy<'a> {
/// Returns `true` if the hash policy is `None`.
pub fn is_none(&self) -> bool {
matches!(self, HashPolicy::None)
}

/// Returns `true` if the hash policy is `Generate`.
pub fn is_generate(&self) -> bool {
matches!(self, HashPolicy::Generate)
}

/// Returns `true` if the hash policy is `Validate`.
pub fn is_validate(&self) -> bool {
matches!(self, HashPolicy::Validate(_))
}

/// Return the algorithms used in the hash policy.
pub fn algorithms(&self) -> Vec<HashAlgorithm> {
match self {
HashPolicy::None => vec![],
HashPolicy::Generate => vec![HashAlgorithm::Sha256],
HashPolicy::Validate(hashes) => {
let mut algorithms = hashes.iter().map(HashDigest::algorithm).collect::<Vec<_>>();
algorithms.sort();
algorithms.dedup();
algorithms
}
}
}

/// Return the digests used in the hash policy.
pub fn digests(&self) -> &[HashDigest] {
match self {
HashPolicy::None => &[],
HashPolicy::Generate => &[],
HashPolicy::Validate(hashes) => hashes,
}
}
}
39 changes: 15 additions & 24 deletions crates/uv-client/src/flat_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use url::Url;

use distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
use distribution_types::{
BuiltDist, Dist, File, FileLocation, FlatIndexLocation, IncompatibleSource, IncompatibleWheel,
IndexUrl, PrioritizedDist, RegistryBuiltDist, RegistrySourceDist, SourceDist,
SourceDistCompatibility, WheelCompatibility,
BuiltDist, Dist, File, FileLocation, FlatIndexLocation, HashPolicy, IncompatibleSource,
IncompatibleWheel, IndexUrl, PrioritizedDist, RegistryBuiltDist, RegistrySourceDist,
SourceDist, SourceDistCompatibility, WheelCompatibility,
};
use pep440_rs::Version;
use pep508_rs::VerbatimUrl;
Expand All @@ -21,7 +21,7 @@ use pypi_types::HashDigest;
use uv_cache::{Cache, CacheBucket};
use uv_configuration::{NoBinary, NoBuild};
use uv_normalize::PackageName;
use uv_types::RequiredHashes;
use uv_types::Hasher;

use crate::cached_client::{CacheControl, CachedClientError};
use crate::html::SimpleHtml;
Expand Down Expand Up @@ -277,7 +277,7 @@ impl FlatIndex {
pub fn from_entries(
entries: FlatIndexEntries,
tags: &Tags,
required_hashes: &RequiredHashes,
hasher: &Hasher,
no_build: &NoBuild,
no_binary: &NoBinary,
) -> Self {
Expand All @@ -290,7 +290,7 @@ impl FlatIndex {
file,
filename,
tags,
required_hashes,
hasher,
no_build,
no_binary,
url,
Expand All @@ -309,7 +309,7 @@ impl FlatIndex {
file: File,
filename: DistFilename,
tags: &Tags,
required_hashes: &RequiredHashes,
hasher: &Hasher,
no_build: &NoBuild,
no_binary: &NoBinary,
index: IndexUrl,
Expand All @@ -320,13 +320,8 @@ impl FlatIndex {
DistFilename::WheelFilename(filename) => {
let version = filename.version.clone();

let compatibility = Self::wheel_compatibility(
&filename,
&file.hashes,
tags,
required_hashes,
no_binary,
);
let compatibility =
Self::wheel_compatibility(&filename, &file.hashes, tags, hasher, no_binary);
let dist = Dist::Built(BuiltDist::Registry(RegistryBuiltDist {
filename,
file: Box::new(file),
Expand All @@ -342,12 +337,8 @@ impl FlatIndex {
}
}
DistFilename::SourceDistFilename(filename) => {
let compatibility = Self::source_dist_compatibility(
&filename,
&file.hashes,
required_hashes,
no_build,
);
let compatibility =
Self::source_dist_compatibility(&filename, &file.hashes, hasher, no_build);
let dist = Dist::Source(SourceDist::Registry(RegistrySourceDist {
filename: filename.clone(),
file: Box::new(file),
Expand All @@ -368,7 +359,7 @@ impl FlatIndex {
fn source_dist_compatibility(
filename: &SourceDistFilename,
hashes: &[HashDigest],
required_hashes: &RequiredHashes,
hasher: &Hasher,
no_build: &NoBuild,
) -> SourceDistCompatibility {
// Check if source distributions are allowed for this package.
Expand All @@ -383,7 +374,7 @@ impl FlatIndex {
}

// Check if hashes line up
if let Some(required_hashes) = required_hashes.get(&filename.name) {
if let HashPolicy::Validate(required_hashes) = hasher.get(&filename.name) {
if !required_hashes.is_empty() {
if hashes.is_empty() {
return SourceDistCompatibility::Incompatible(IncompatibleSource::MissingHash);
Expand All @@ -403,7 +394,7 @@ impl FlatIndex {
filename: &WheelFilename,
hashes: &[HashDigest],
tags: &Tags,
required_hashes: &RequiredHashes,
hasher: &Hasher,
no_binary: &NoBinary,
) -> WheelCompatibility {
// Check if binaries are allowed for this package.
Expand All @@ -418,7 +409,7 @@ impl FlatIndex {
}

// Check if hashes line up
if let Some(required_hashes) = required_hashes.get(&filename.name) {
if let HashPolicy::Validate(required_hashes) = hasher.get(&filename.name) {
if !required_hashes.is_empty() {
if hashes.is_empty() {
return WheelCompatibility::Incompatible(IncompatibleWheel::MissingHash);
Expand Down
7 changes: 3 additions & 4 deletions crates/uv-dev/src/resolve_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use uv_dispatch::BuildDispatch;
use uv_installer::SitePackages;
use uv_interpreter::PythonEnvironment;
use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver};
use uv_types::{BuildIsolation, InFlight, RequiredHashes};
use uv_types::{BuildIsolation, Hasher, InFlight};

#[derive(ValueEnum, Default, Clone)]
pub(crate) enum ResolveCliFormat {
Expand Down Expand Up @@ -58,7 +58,6 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> {
let index_locations =
IndexLocations::new(args.index_url, args.extra_index_url, args.find_links, false);
let index = InMemoryIndex::default();
let hashes = RequiredHashes::default();
let in_flight = InFlight::default();
let no_build = if args.no_build {
NoBuild::All
Expand All @@ -74,7 +73,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> {
FlatIndex::from_entries(
entries,
venv.interpreter().tags()?,
&RequiredHashes::default(),
&Hasher::None,
&no_build,
&NoBinary::None,
)
Expand Down Expand Up @@ -109,7 +108,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> {
&client,
&flat_index,
&index,
&hashes,
&Hasher::None,
&build_dispatch,
&site_packages,
)?;
Expand Down
13 changes: 5 additions & 8 deletions crates/uv-dispatch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use uv_configuration::{BuildKind, ConfigSettings, NoBinary, NoBuild, Reinstall,
use uv_installer::{Downloader, Installer, Plan, Planner, SitePackages};
use uv_interpreter::{Interpreter, PythonEnvironment};
use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver};
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, InFlight, RequiredHashes};
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, Hasher, InFlight};

/// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`]
/// documentation.
Expand Down Expand Up @@ -134,7 +134,6 @@ impl<'a> BuildContext for BuildDispatch<'a> {
async fn resolve<'data>(&'data self, requirements: &'data [Requirement]) -> Result<Resolution> {
let markers = self.interpreter.markers();
let tags = self.interpreter.tags()?;
let hashes = RequiredHashes::default();
let resolver = Resolver::new(
Manifest::simple(requirements.to_vec()),
self.options,
Expand All @@ -144,7 +143,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
self.client,
self.flat_index,
self.index,
&hashes,
&Hasher::None,
self,
&EmptyInstalledPackages,
)?;
Expand Down Expand Up @@ -180,9 +179,6 @@ impl<'a> BuildContext for BuildDispatch<'a> {
venv.root().display(),
);

// Don't enforce hashes for build dependencies.
let hashes = RequiredHashes::default();

// Determine the current environment markers.
let tags = self.interpreter.tags()?;

Expand All @@ -199,7 +195,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
site_packages,
&Reinstall::None,
&NoBinary::None,
&RequiredHashes::default(),
&Hasher::None,
self.index_locations,
self.cache(),
venv,
Expand Down Expand Up @@ -228,7 +224,8 @@ impl<'a> BuildContext for BuildDispatch<'a> {
vec![]
} else {
// TODO(konstin): Check that there is no endless recursion.
let downloader = Downloader::new(self.cache, tags, &hashes, self.client, self);
let downloader =
Downloader::new(self.cache, tags, &Hasher::None, self.client, self);
debug!(
"Downloading and building requirement{} for build: {}",
if remote.len() == 1 { "" } else { "s" },
Expand Down
Loading

0 comments on commit 446ffd2

Please sign in to comment.