Skip to main content

uv_distribution_types/
lib.rs

1//! ## Type hierarchy
2//!
3//! When we receive the requirements from `pip sync`, we check which requirements already fulfilled
4//! in the users environment ([`InstalledDist`]), whether the matching package is in our wheel cache
5//! ([`CachedDist`]) or whether we need to download, (potentially build) and install it ([`Dist`]).
6//!
7//! ## `Dist`
8//! A [`Dist`] is either a built distribution (a wheel), or a source distribution that exists at
9//! some location. We translate every PEP 508 requirement e.g. from `requirements.txt` or from
10//! `pyproject.toml`'s `[project] dependencies` into a [`Dist`] by checking each index.
11//! * [`BuiltDist`]: A wheel, with its four possible origins:
12//!   * [`RegistryBuiltDist`]
13//!   * [`DirectUrlBuiltDist`]
14//!   * [`PathBuiltDist`]
15//!   * [`GitPathBuiltDist`]
16//! * [`SourceDist`]: A source distribution, with its six possible origins:
17//!   * [`RegistrySourceDist`]
18//!   * [`DirectUrlSourceDist`]
19//!   * [`GitDirectorySourceDist`]
20//!   * [`GitPathSourceDist`]
21//!   * [`PathSourceDist`]
22//!   * [`DirectorySourceDist`]
23//!
24//! ## `CachedDist`
25//! A [`CachedDist`] is a built distribution (wheel) that exists in the local cache, with the two
26//! possible origins we currently track:
27//! * [`CachedRegistryDist`]
28//! * [`CachedDirectUrlDist`]
29//!
30//! ## `InstalledDist`
31//! An [`InstalledDist`] is a distribution installed in a Python environment, with the five kinds
32//! we currently track:
33//! * [`InstalledRegistryDist`]
34//! * [`InstalledDirectUrlDist`]
35//! * [`InstalledEggInfoFile`]
36//! * [`InstalledEggInfoDirectory`]
37//! * [`InstalledLegacyEditable`]
38//!
39//! Direct URL information for an [`InstalledDirectUrlDist`] comes from
40//! [`direct_url.json`](https://packaging.python.org/en/latest/specifications/direct-url-data-structure/)
41//! and may not match the original [`Dist`] exactly.
42use std::borrow::Cow;
43use std::ffi::OsStr;
44use std::fmt::Display;
45use std::path;
46use std::path::{Path, PathBuf};
47use std::str::FromStr;
48
49use memchr::memchr3;
50use url::Url;
51
52use uv_distribution_filename::{
53    DistExtension, SourceDistExtension, SourceDistFilename, WheelFilename,
54};
55use uv_fs::normalize_absolute_path;
56use uv_git_types::GitUrl;
57use uv_normalize::PackageName;
58use uv_pep440::Version;
59use uv_pep508::{Pep508Url, VerbatimUrl};
60use uv_pypi_types::{
61    ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitDirectoryUrl, ParsedGitPathUrl, ParsedPathUrl,
62    ParsedUrl, VerbatimParsedUrl,
63};
64use uv_redacted::DisplaySafeUrl;
65
66pub use crate::annotation::*;
67pub use crate::any::*;
68pub use crate::build_info::*;
69pub use crate::build_requires::*;
70pub use crate::buildable::*;
71pub use crate::cached::*;
72pub use crate::config_settings::*;
73pub use crate::dependency_metadata::*;
74pub use crate::diagnostic::*;
75pub use crate::dist_error::*;
76pub use crate::error::*;
77pub use crate::exclude_newer::*;
78pub use crate::file::*;
79pub use crate::hash::*;
80pub use crate::id::*;
81pub use crate::index::*;
82pub use crate::index_name::*;
83pub use crate::index_url::*;
84pub use crate::installed::*;
85pub use crate::known_platform::*;
86pub use crate::minimum_libc_version::MinimumLibcVersion;
87pub use crate::origin::*;
88pub use crate::pip_index::*;
89pub use crate::prioritized_distribution::*;
90pub use crate::requested::*;
91pub use crate::requirement::*;
92pub use crate::requires_python::*;
93pub use crate::resolution::*;
94pub use crate::resolution_recorder::*;
95pub use crate::resolved::*;
96pub use crate::specified_requirement::*;
97pub use crate::status_code_strategy::*;
98pub use crate::traits::*;
99
100mod annotation;
101mod any;
102mod build_info;
103mod build_requires;
104mod buildable;
105mod cached;
106mod config_settings;
107mod dependency_metadata;
108mod diagnostic;
109mod dist_error;
110mod error;
111mod exclude_newer;
112mod file;
113mod hash;
114mod id;
115mod index;
116mod index_name;
117mod index_url;
118mod installed;
119mod installed_modules;
120mod known_platform;
121mod minimum_libc_version;
122mod origin;
123mod pip_index;
124mod prioritized_distribution;
125mod requested;
126mod requirement;
127mod requires_python;
128mod resolution;
129mod resolution_recorder;
130mod resolved;
131mod specified_requirement;
132mod status_code_strategy;
133mod traits;
134
135#[derive(Debug, Clone)]
136pub enum VersionOrUrlRef<'a, T: Pep508Url = VerbatimUrl> {
137    /// A PEP 440 version specifier, used to identify a distribution in a registry.
138    Version(&'a Version),
139    /// A URL, used to identify a distribution at an arbitrary location.
140    Url(&'a T),
141}
142
143impl Verbatim for VersionOrUrlRef<'_> {
144    fn verbatim(&self) -> Cow<'_, str> {
145        match self {
146            Self::Version(version) => Cow::Owned(format!("=={version}")),
147            Self::Url(url) => Cow::Owned(format!(" @ {}", url.verbatim())),
148        }
149    }
150}
151
152impl std::fmt::Display for VersionOrUrlRef<'_> {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        match self {
155            Self::Version(version) => write!(f, "=={version}"),
156            Self::Url(url) => write!(f, " @ {url}"),
157        }
158    }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
162pub enum InstalledVersion<'a> {
163    /// A PEP 440 version specifier, used to identify a distribution in a registry.
164    Version(&'a Version),
165    /// A URL, used to identify a distribution at an arbitrary location, along with the version
166    /// specifier to which it resolved.
167    Url(&'a DisplaySafeUrl, &'a Version),
168}
169
170impl<'a> InstalledVersion<'a> {
171    /// If it is a version, return its value.
172    pub fn version(&self) -> &'a Version {
173        match self {
174            Self::Version(version) => version,
175            Self::Url(_, version) => version,
176        }
177    }
178}
179
180impl std::fmt::Display for InstalledVersion<'_> {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        match self {
183            Self::Version(version) => write!(f, "=={version}"),
184            Self::Url(url, version) => write!(f, "=={version} (from {url})"),
185        }
186    }
187}
188
189/// Either a built distribution (a wheel) or a source distribution that exists at some location.
190///
191/// The location can be an index, URL, path, or Git repository (wheel or source distribution).
192#[derive(Debug, Clone, Hash, PartialEq, Eq)]
193pub enum Dist {
194    Built(BuiltDist),
195    Source(SourceDist),
196}
197
198/// A reference to a built or source distribution.
199#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
200pub enum DistRef<'a> {
201    Built(&'a BuiltDist),
202    Source(&'a SourceDist),
203}
204
205impl Display for DistRef<'_> {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        match self {
208            Self::Built(built_dist) => Display::fmt(&built_dist, f),
209            Self::Source(source_dist) => Display::fmt(&source_dist, f),
210        }
211    }
212}
213
214/// A wheel, with its four possible origins (index, URL, path, or Git path)
215#[derive(Debug, Clone, Hash, PartialEq, Eq)]
216pub enum BuiltDist {
217    Registry(RegistryBuiltDist),
218    DirectUrl(DirectUrlBuiltDist),
219    Path(PathBuiltDist),
220    GitPath(GitPathBuiltDist),
221}
222
223/// A source distribution, with its six possible origins (index, URL, Git directory, Git path,
224/// path, or directory).
225#[derive(Debug, Clone, Hash, PartialEq, Eq)]
226pub enum SourceDist {
227    Registry(RegistrySourceDist),
228    DirectUrl(DirectUrlSourceDist),
229    GitDirectory(GitDirectorySourceDist),
230    GitPath(GitPathSourceDist),
231    Path(PathSourceDist),
232    Directory(DirectorySourceDist),
233}
234
235/// A built distribution (wheel) that exists in a registry, like `PyPI`.
236#[derive(Debug, Clone, Hash, PartialEq, Eq)]
237pub struct RegistryBuiltWheel {
238    pub filename: WheelFilename,
239    pub file: Box<File>,
240    pub index: IndexUrl,
241    /// Whether the recorded size must be validated when the wheel is downloaded.
242    pub size_is_authoritative: bool,
243}
244
245/// A built distribution (wheel) that exists in a registry, like `PyPI`.
246#[derive(Debug, Clone, Hash, PartialEq, Eq)]
247pub struct RegistryBuiltDist {
248    /// All wheels associated with this distribution. It is guaranteed
249    /// that there is at least one wheel.
250    pub wheels: Vec<RegistryBuiltWheel>,
251    /// The "best" wheel selected based on the current wheel tag
252    /// environment.
253    ///
254    /// This is guaranteed to point into a valid entry in `wheels`.
255    pub best_wheel_index: usize,
256    /// A source distribution if one exists for this distribution.
257    ///
258    /// It is possible for this to be `None`. For example, when a distribution
259    /// has no source distribution, or if it does have one but isn't compatible
260    /// with the user configuration. (e.g., If `Requires-Python` isn't
261    /// compatible with the installed/target Python versions, or if something
262    /// like `--exclude-newer` was used.)
263    pub sdist: Option<RegistrySourceDist>,
264    // Ideally, this type would have an index URL on it, and the
265    // `RegistryBuiltDist` and `RegistrySourceDist` types would *not* have an
266    // index URL on them. Alas, the --find-links feature makes it technically
267    // possible for the indexes to diverge across wheels/sdists in the same
268    // distribution.
269    //
270    // Note though that at time of writing, when generating a universal lock
271    // file, we require that all index URLs across wheels/sdists for a single
272    // distribution are equivalent.
273}
274
275/// A built distribution (wheel) that exists at an arbitrary URL.
276#[derive(Debug, Clone, Hash, PartialEq, Eq)]
277pub struct DirectUrlBuiltDist {
278    /// We require that wheel urls end in the full wheel filename, e.g.
279    /// `https://example.org/packages/flask-3.0.0-py3-none-any.whl`
280    pub filename: WheelFilename,
281    /// The URL without the subdirectory fragment.
282    pub location: Box<DisplaySafeUrl>,
283    /// The URL as it was provided by the user.
284    pub url: VerbatimUrl,
285    /// The expected size of the archive, if provided by a lockfile.
286    pub size: Option<u64>,
287}
288
289/// A built distribution (wheel) that exists in a local directory.
290#[derive(Debug, Clone, Hash, PartialEq, Eq)]
291pub struct PathBuiltDist {
292    pub filename: WheelFilename,
293    /// The absolute path to the wheel which we use for installing.
294    pub install_path: Box<Path>,
295    /// The URL as it was provided by the user.
296    pub url: VerbatimUrl,
297}
298
299/// A built distribution (wheel) that exists in a Git repository.
300#[derive(Debug, Clone, Hash, PartialEq, Eq)]
301pub struct GitPathBuiltDist {
302    pub filename: WheelFilename,
303    /// The URL without the revision and path fragment.
304    pub git: Box<GitUrl>,
305    /// The path within the Git repository to the distribution which we use for installing.
306    pub install_path: PathBuf,
307    /// The URL as it was provided by the user, including the revision and path fragment.
308    pub url: VerbatimUrl,
309}
310
311/// A source distribution that exists in a registry, like `PyPI`.
312#[derive(Debug, Clone, Hash, PartialEq, Eq)]
313pub struct RegistrySourceDist {
314    pub name: PackageName,
315    pub version: Version,
316    pub file: Box<File>,
317    /// The file extension, e.g. `tar.gz`, `zip`, etc.
318    pub ext: SourceDistExtension,
319    pub index: IndexUrl,
320    /// When an sdist is selected, it may be the case that there were
321    /// available wheels too. There are many reasons why a wheel might not
322    /// have been chosen (maybe none available are compatible with the
323    /// current environment), but we still want to track that they exist. In
324    /// particular, for generating a universal lockfile, we do not want to
325    /// skip emitting wheels to the lockfile just because the host generating
326    /// the lockfile didn't have any compatible wheels available.
327    pub wheels: Vec<RegistryBuiltWheel>,
328    /// Whether the recorded size must be validated when the source distribution is downloaded.
329    pub size_is_authoritative: bool,
330}
331
332/// A source distribution that exists at an arbitrary URL.
333#[derive(Debug, Clone, Hash, PartialEq, Eq)]
334pub struct DirectUrlSourceDist {
335    /// Unlike [`DirectUrlBuiltDist`], we can't require a full filename with a version here, people
336    /// like using e.g. `foo @ https://github.com/org/repo/archive/master.zip`
337    pub name: PackageName,
338    /// The URL without the subdirectory fragment.
339    pub location: Box<DisplaySafeUrl>,
340    /// The subdirectory within the archive in which the source distribution is located.
341    pub subdirectory: Option<Box<Path>>,
342    /// The file extension, e.g. `tar.gz`, `zip`, etc.
343    pub ext: SourceDistExtension,
344    /// The URL as it was provided by the user, including the subdirectory fragment.
345    pub url: VerbatimUrl,
346    /// The expected size of the archive, if provided by a lockfile.
347    pub size: Option<u64>,
348}
349
350/// A source distribution that exists at the root or in a subdirectory of a Git repository.
351#[derive(Debug, Clone, Hash, PartialEq, Eq)]
352pub struct GitDirectorySourceDist {
353    pub name: PackageName,
354    /// The URL without the revision and subdirectory fragment.
355    pub git: Box<GitUrl>,
356    /// The subdirectory within the Git repository in which the source distribution is located.
357    pub subdirectory: Option<Box<Path>>,
358    /// The URL as it was provided by the user, including the revision and subdirectory fragment.
359    pub url: VerbatimUrl,
360}
361
362/// A source distribution that exists in a local archive (e.g., a `.tar.gz` file) within a Git
363/// repository.
364#[derive(Debug, Clone, Hash, PartialEq, Eq)]
365pub struct GitPathSourceDist {
366    pub name: PackageName,
367    /// The URL without the revision and subdirectory fragment.
368    pub git: Box<GitUrl>,
369    /// The path within the Git repository to the distribution which we use for installing.
370    pub install_path: PathBuf,
371    /// The file extension, e.g. `tar.gz`, `zip`, etc.
372    pub ext: SourceDistExtension,
373    /// The URL as it was provided by the user, including the revision and subdirectory fragment.
374    pub url: VerbatimUrl,
375}
376
377/// A source distribution that exists in a local archive (e.g., a `.tar.gz` file).
378#[derive(Debug, Clone, Hash, PartialEq, Eq)]
379pub struct PathSourceDist {
380    pub name: PackageName,
381    pub version: Option<Version>,
382    /// The absolute path to the distribution which we use for installing.
383    pub install_path: Box<Path>,
384    /// The file extension, e.g. `tar.gz`, `zip`, etc.
385    pub ext: SourceDistExtension,
386    /// The URL as it was provided by the user.
387    pub url: VerbatimUrl,
388}
389
390/// Whether a source distribution is a first-party workspace member.
391#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
392pub enum FirstParty {
393    Yes,
394    No,
395}
396
397/// A source distribution that exists in a local directory.
398#[derive(Debug, Clone, Hash, PartialEq, Eq)]
399pub struct DirectorySourceDist {
400    pub name: PackageName,
401    /// The absolute path to the distribution which we use for installing.
402    pub install_path: Box<Path>,
403    /// Whether the package should be installed in editable mode.
404    pub editable: Option<bool>,
405    /// Whether the package should be built and installed.
406    pub r#virtual: Option<bool>,
407    /// Whether the package is a first-party workspace member.
408    pub first_party: FirstParty,
409    /// The URL as it was provided by the user.
410    pub url: VerbatimUrl,
411}
412
413impl Dist {
414    /// A remote built distribution (`.whl`) or source distribution from a `http://` or `https://`
415    /// URL.
416    pub fn from_http_url(
417        name: PackageName,
418        url: VerbatimUrl,
419        location: DisplaySafeUrl,
420        subdirectory: Option<Box<Path>>,
421        ext: DistExtension,
422    ) -> Result<Self, Error> {
423        match ext {
424            DistExtension::Wheel => {
425                // Validate that the name in the wheel matches that of the requirement.
426                let filename = WheelFilename::from_str(&url.filename()?)?;
427                if filename.name != name {
428                    return Err(Error::PackageNameMismatch(
429                        name,
430                        filename.name,
431                        url.verbatim().to_string(),
432                    ));
433                }
434
435                Ok(Self::Built(BuiltDist::DirectUrl(DirectUrlBuiltDist {
436                    filename,
437                    location: Box::new(location),
438                    url,
439                    size: None,
440                })))
441            }
442            DistExtension::Source(ext) => {
443                if !ext.is_pep625_compliant() {
444                    return Err(Error::NotPep625Filename(url.verbatim().to_string()));
445                }
446                Ok(Self::Source(SourceDist::DirectUrl(DirectUrlSourceDist {
447                    name,
448                    location: Box::new(location),
449                    subdirectory,
450                    ext,
451                    url,
452                    size: None,
453                })))
454            }
455        }
456    }
457
458    /// A local built or source distribution from a `file://` URL.
459    pub fn from_file_url(
460        name: PackageName,
461        url: VerbatimUrl,
462        install_path: &Path,
463        ext: DistExtension,
464    ) -> Result<Self, Error> {
465        // Convert to an absolute path.
466        let install_path = path::absolute(install_path)?;
467
468        // Normalize the path.
469        let install_path = normalize_absolute_path(&install_path)?;
470
471        // Validate that the path exists.
472        if !install_path.exists() {
473            return Err(Error::NotFound(url.to_url()));
474        }
475
476        // Determine whether the path represents a built or source distribution.
477        match ext {
478            DistExtension::Wheel => {
479                // Validate that the name in the wheel matches that of the requirement.
480                let filename = install_path
481                    .file_name()
482                    .and_then(OsStr::to_str)
483                    .ok_or_else(|| Error::MissingWheelFilename(install_path.clone()))?;
484                let filename = WheelFilename::from_str(filename)?;
485                if filename.name != name {
486                    return Err(Error::PackageNameMismatch(
487                        name,
488                        filename.name,
489                        url.verbatim().to_string(),
490                    ));
491                }
492                Ok(Self::Built(BuiltDist::Path(PathBuiltDist {
493                    filename,
494                    install_path: install_path.into_boxed_path(),
495                    url,
496                })))
497            }
498            DistExtension::Source(ext) => {
499                if !ext.is_pep625_compliant() {
500                    return Err(Error::NotPep625Filename(url.verbatim().to_string()));
501                }
502
503                // If there is a version in the filename, record it.
504                let version = url
505                    .filename()
506                    .ok()
507                    .and_then(|filename| {
508                        SourceDistFilename::parse(filename.as_ref(), ext, &name).ok()
509                    })
510                    .map(|filename| filename.version);
511
512                Ok(Self::Source(SourceDist::Path(PathSourceDist {
513                    name,
514                    version,
515                    install_path: install_path.into_boxed_path(),
516                    ext,
517                    url,
518                })))
519            }
520        }
521    }
522
523    /// A local source tree from a `file://` URL.
524    pub fn from_directory_url(
525        name: PackageName,
526        url: VerbatimUrl,
527        install_path: &Path,
528        editable: Option<bool>,
529        r#virtual: Option<bool>,
530    ) -> Result<Self, Error> {
531        // Convert to an absolute path.
532        let install_path = path::absolute(install_path)?;
533
534        // Normalize the path.
535        let install_path = normalize_absolute_path(&install_path)?;
536
537        // Validate that the path exists.
538        if !install_path.exists() {
539            return Err(Error::NotFound(url.to_url()));
540        }
541
542        // Determine whether the path represents an archive or a directory.
543        Ok(Self::Source(SourceDist::Directory(DirectorySourceDist {
544            name,
545            install_path: install_path.into_boxed_path(),
546            editable,
547            r#virtual,
548            first_party: FirstParty::No,
549            url,
550        })))
551    }
552
553    /// Create a [`Dist`] for a source tree within a Git repository (i.e., a `git+https://` or `git+ssh://` URL).
554    pub fn from_git_directory_url(
555        name: PackageName,
556        url: VerbatimUrl,
557        git: GitUrl,
558        subdirectory: Option<Box<Path>>,
559    ) -> Result<Self, Error> {
560        Ok(Self::Source(SourceDist::GitDirectory(
561            GitDirectorySourceDist {
562                name,
563                git: Box::new(git),
564                subdirectory,
565                url,
566            },
567        )))
568    }
569
570    /// Create a [`Dist`] for a source archive within a Git repository (i.e., a `git+https://` or `git+ssh://` URL).
571    pub fn from_git_path_url(
572        name: PackageName,
573        url: VerbatimUrl,
574        git: GitUrl,
575        install_path: PathBuf,
576        ext: DistExtension,
577    ) -> Result<Self, Error> {
578        match ext {
579            DistExtension::Wheel => {
580                // Validate that the name in the wheel matches that of the requirement.
581                let filename = install_path
582                    .file_name()
583                    .and_then(OsStr::to_str)
584                    .ok_or_else(|| Error::MissingWheelFilename(install_path.clone()))?;
585                let filename = WheelFilename::from_str(filename)?;
586                if filename.name != name {
587                    return Err(Error::PackageNameMismatch(
588                        name,
589                        filename.name,
590                        url.verbatim().to_string(),
591                    ));
592                }
593
594                Ok(Self::Built(BuiltDist::GitPath(GitPathBuiltDist {
595                    filename,
596                    git: Box::new(git),
597                    install_path,
598                    url,
599                })))
600            }
601            DistExtension::Source(ext) => {
602                Ok(Self::Source(SourceDist::GitPath(GitPathSourceDist {
603                    name,
604                    git: Box::new(git),
605                    install_path,
606                    ext,
607                    url,
608                })))
609            }
610        }
611    }
612
613    /// Create a [`Dist`] for a URL-based distribution.
614    pub fn from_url(name: PackageName, url: VerbatimParsedUrl) -> Result<Self, Error> {
615        match url.parsed_url {
616            ParsedUrl::Archive(archive) => Self::from_http_url(
617                name,
618                url.verbatim,
619                archive.url,
620                archive.subdirectory,
621                archive.ext,
622            ),
623            ParsedUrl::Path(file) => {
624                Self::from_file_url(name, url.verbatim, &file.install_path, file.ext)
625            }
626            ParsedUrl::Directory(directory) => Self::from_directory_url(
627                name,
628                url.verbatim,
629                &directory.install_path,
630                directory.editable,
631                directory.r#virtual,
632            ),
633            ParsedUrl::GitDirectory(git) => {
634                Self::from_git_directory_url(name, url.verbatim, git.url, git.subdirectory)
635            }
636            ParsedUrl::GitPath(git) => {
637                Self::from_git_path_url(name, url.verbatim, git.url, git.install_path, git.ext)
638            }
639        }
640    }
641
642    /// Return true if the distribution is editable.
643    fn is_editable(&self) -> bool {
644        match self {
645            Self::Source(dist) => dist.is_editable(),
646            Self::Built(_) => false,
647        }
648    }
649
650    /// Return true if the distribution refers to a local file or directory.
651    fn is_local(&self) -> bool {
652        match self {
653            Self::Source(dist) => dist.is_local(),
654            Self::Built(dist) => dist.is_local(),
655        }
656    }
657
658    /// Returns the [`IndexUrl`], if the distribution is from a registry.
659    pub fn index(&self) -> Option<&IndexUrl> {
660        match self {
661            Self::Built(dist) => dist.index(),
662            Self::Source(dist) => dist.index(),
663        }
664    }
665
666    /// Returns the [`File`] instance, if this dist is from a registry with simple json api support
667    pub fn file(&self) -> Option<&File> {
668        match self {
669            Self::Built(built) => built.file(),
670            Self::Source(source) => source.file(),
671        }
672    }
673
674    /// Return the source tree of the distribution, if available.
675    pub fn source_tree(&self) -> Option<&Path> {
676        match self {
677            Self::Built { .. } => None,
678            Self::Source(source) => source.source_tree(),
679        }
680    }
681
682    /// Returns the version of the distribution, if it is known.
683    pub fn version(&self) -> Option<&Version> {
684        match self {
685            Self::Built(wheel) => Some(wheel.version()),
686            Self::Source(source_dist) => source_dist.version(),
687        }
688    }
689}
690
691impl<'a> From<&'a Dist> for DistRef<'a> {
692    fn from(dist: &'a Dist) -> Self {
693        match dist {
694            Dist::Built(built) => DistRef::Built(built),
695            Dist::Source(source) => DistRef::Source(source),
696        }
697    }
698}
699
700impl<'a> From<&'a SourceDist> for DistRef<'a> {
701    fn from(dist: &'a SourceDist) -> Self {
702        DistRef::Source(dist)
703    }
704}
705
706impl<'a> From<&'a BuiltDist> for DistRef<'a> {
707    fn from(dist: &'a BuiltDist) -> Self {
708        DistRef::Built(dist)
709    }
710}
711
712impl BuiltDist {
713    /// Return true if the distribution refers to a local file or directory.
714    fn is_local(&self) -> bool {
715        matches!(self, Self::Path(_))
716    }
717
718    /// Returns the [`IndexUrl`], if the distribution is from a registry.
719    pub fn index(&self) -> Option<&IndexUrl> {
720        match self {
721            Self::Registry(registry) => Some(&registry.best_wheel().index),
722            Self::DirectUrl(_) => None,
723            Self::Path(_) => None,
724            Self::GitPath(_) => None,
725        }
726    }
727
728    /// Returns the [`File`] instance, if this distribution is from a registry.
729    fn file(&self) -> Option<&File> {
730        match self {
731            Self::Registry(registry) => Some(&registry.best_wheel().file),
732            Self::DirectUrl(_) | Self::Path(_) | Self::GitPath(_) => None,
733        }
734    }
735
736    pub fn version(&self) -> &Version {
737        match self {
738            Self::Registry(wheels) => &wheels.best_wheel().filename.version,
739            Self::DirectUrl(wheel) => &wheel.filename.version,
740            Self::Path(wheel) => &wheel.filename.version,
741            Self::GitPath(wheel) => &wheel.filename.version,
742        }
743    }
744}
745
746impl SourceDist {
747    /// Returns the [`IndexUrl`], if the distribution is from a registry.
748    fn index(&self) -> Option<&IndexUrl> {
749        match self {
750            Self::Registry(registry) => Some(&registry.index),
751            Self::DirectUrl(_)
752            | Self::GitPath(_)
753            | Self::GitDirectory(_)
754            | Self::Path(_)
755            | Self::Directory(_) => None,
756        }
757    }
758
759    /// Returns the [`File`] instance, if this dist is from a registry with simple json api support
760    fn file(&self) -> Option<&File> {
761        match self {
762            Self::Registry(registry) => Some(&registry.file),
763            Self::DirectUrl(_)
764            | Self::GitPath(_)
765            | Self::GitDirectory(_)
766            | Self::Path(_)
767            | Self::Directory(_) => None,
768        }
769    }
770
771    /// Returns the [`Version`] of the distribution, if it is known.
772    pub fn version(&self) -> Option<&Version> {
773        match self {
774            Self::Registry(source_dist) => Some(&source_dist.version),
775            Self::DirectUrl(_)
776            | Self::GitPath(_)
777            | Self::GitDirectory(_)
778            | Self::Path(_)
779            | Self::Directory(_) => None,
780        }
781    }
782
783    /// Returns `true` if the distribution is editable.
784    pub fn is_editable(&self) -> bool {
785        match self {
786            Self::Directory(DirectorySourceDist { editable, .. }) => editable.unwrap_or(false),
787            _ => false,
788        }
789    }
790
791    /// Returns `true` if the distribution is virtual.
792    pub fn is_virtual(&self) -> bool {
793        match self {
794            Self::Directory(DirectorySourceDist { r#virtual, .. }) => r#virtual.unwrap_or(false),
795            _ => false,
796        }
797    }
798
799    /// Returns `true` if the distribution is a first-party workspace member.
800    pub fn is_first_party(&self) -> bool {
801        match self {
802            Self::Directory(DirectorySourceDist {
803                first_party: FirstParty::Yes,
804                ..
805            }) => true,
806            Self::Directory(DirectorySourceDist {
807                first_party: FirstParty::No,
808                ..
809            })
810            | Self::Registry(_)
811            | Self::DirectUrl(_)
812            | Self::GitDirectory(_)
813            | Self::GitPath(_)
814            | Self::Path(_) => false,
815        }
816    }
817
818    /// Returns `true` if the distribution refers to a local file or directory.
819    fn is_local(&self) -> bool {
820        matches!(self, Self::Directory(_) | Self::Path(_))
821    }
822
823    /// Returns the path to the source distribution, if it's a local distribution.
824    pub fn as_path(&self) -> Option<&Path> {
825        match self {
826            Self::Path(dist) => Some(&dist.install_path),
827            Self::Directory(dist) => Some(&dist.install_path),
828            _ => None,
829        }
830    }
831
832    /// Returns the source tree of the distribution, if available.
833    fn source_tree(&self) -> Option<&Path> {
834        match self {
835            Self::Directory(dist) => Some(&dist.install_path),
836            _ => None,
837        }
838    }
839}
840
841impl RegistryBuiltDist {
842    /// Returns the best or "most compatible" wheel in this distribution.
843    pub fn best_wheel(&self) -> &RegistryBuiltWheel {
844        &self.wheels[self.best_wheel_index]
845    }
846}
847
848impl DirectUrlBuiltDist {
849    /// Return the [`ParsedUrl`] for the distribution.
850    pub fn to_parsed_url(&self) -> ParsedUrl {
851        ParsedUrl::Archive(ParsedArchiveUrl::from_source(
852            (*self.location).clone(),
853            None,
854            DistExtension::Wheel,
855        ))
856    }
857}
858
859impl PathBuiltDist {
860    /// Return the [`ParsedUrl`] for the distribution.
861    pub fn to_parsed_url(&self) -> ParsedUrl {
862        ParsedUrl::Path(ParsedPathUrl::from_source(
863            self.install_path.clone(),
864            DistExtension::Wheel,
865            self.url.to_url(),
866        ))
867    }
868}
869
870impl PathSourceDist {
871    /// Return the [`ParsedUrl`] for the distribution.
872    pub fn to_parsed_url(&self) -> ParsedUrl {
873        ParsedUrl::Path(ParsedPathUrl::from_source(
874            self.install_path.clone(),
875            DistExtension::Source(self.ext),
876            self.url.to_url(),
877        ))
878    }
879}
880
881impl DirectUrlSourceDist {
882    /// Return the [`ParsedUrl`] for the distribution.
883    pub fn to_parsed_url(&self) -> ParsedUrl {
884        ParsedUrl::Archive(ParsedArchiveUrl::from_source(
885            (*self.location).clone(),
886            self.subdirectory.clone(),
887            DistExtension::Source(self.ext),
888        ))
889    }
890}
891
892impl GitDirectorySourceDist {
893    /// Return the [`ParsedUrl`] for the distribution.
894    pub fn to_parsed_url(&self) -> ParsedUrl {
895        ParsedUrl::GitDirectory(ParsedGitDirectoryUrl::from_source(
896            (*self.git).clone(),
897            self.subdirectory.clone(),
898        ))
899    }
900}
901
902impl GitPathBuiltDist {
903    /// Return the [`ParsedUrl`] for the distribution.
904    pub fn to_parsed_url(&self) -> ParsedUrl {
905        ParsedUrl::GitPath(ParsedGitPathUrl::from_source(
906            (*self.git).clone(),
907            self.install_path.clone(),
908            DistExtension::Wheel,
909        ))
910    }
911}
912
913impl GitPathSourceDist {
914    /// Return the [`ParsedUrl`] for the distribution.
915    pub fn to_parsed_url(&self) -> ParsedUrl {
916        ParsedUrl::GitPath(ParsedGitPathUrl::from_source(
917            (*self.git).clone(),
918            self.install_path.clone(),
919            DistExtension::Source(self.ext),
920        ))
921    }
922}
923
924impl DirectorySourceDist {
925    /// Return the [`ParsedUrl`] for the distribution.
926    pub fn to_parsed_url(&self) -> ParsedUrl {
927        ParsedUrl::Directory(ParsedDirectoryUrl::from_source(
928            self.install_path.clone(),
929            self.editable,
930            self.r#virtual,
931            self.url.to_url(),
932        ))
933    }
934}
935
936impl Name for RegistryBuiltWheel {
937    fn name(&self) -> &PackageName {
938        &self.filename.name
939    }
940}
941
942impl Name for RegistryBuiltDist {
943    fn name(&self) -> &PackageName {
944        self.best_wheel().name()
945    }
946}
947
948impl Name for DirectUrlBuiltDist {
949    fn name(&self) -> &PackageName {
950        &self.filename.name
951    }
952}
953
954impl Name for PathBuiltDist {
955    fn name(&self) -> &PackageName {
956        &self.filename.name
957    }
958}
959
960impl Name for GitPathBuiltDist {
961    fn name(&self) -> &PackageName {
962        &self.filename.name
963    }
964}
965
966impl Name for RegistrySourceDist {
967    fn name(&self) -> &PackageName {
968        &self.name
969    }
970}
971
972impl Name for DirectUrlSourceDist {
973    fn name(&self) -> &PackageName {
974        &self.name
975    }
976}
977
978impl Name for GitPathSourceDist {
979    fn name(&self) -> &PackageName {
980        &self.name
981    }
982}
983
984impl Name for GitDirectorySourceDist {
985    fn name(&self) -> &PackageName {
986        &self.name
987    }
988}
989
990impl Name for PathSourceDist {
991    fn name(&self) -> &PackageName {
992        &self.name
993    }
994}
995
996impl Name for DirectorySourceDist {
997    fn name(&self) -> &PackageName {
998        &self.name
999    }
1000}
1001
1002impl Name for SourceDist {
1003    fn name(&self) -> &PackageName {
1004        match self {
1005            Self::Registry(dist) => dist.name(),
1006            Self::DirectUrl(dist) => dist.name(),
1007            Self::GitPath(dist) => dist.name(),
1008            Self::GitDirectory(dist) => dist.name(),
1009            Self::Path(dist) => dist.name(),
1010            Self::Directory(dist) => dist.name(),
1011        }
1012    }
1013}
1014
1015impl Name for BuiltDist {
1016    fn name(&self) -> &PackageName {
1017        match self {
1018            Self::Registry(dist) => dist.name(),
1019            Self::DirectUrl(dist) => dist.name(),
1020            Self::Path(dist) => dist.name(),
1021            Self::GitPath(dist) => dist.name(),
1022        }
1023    }
1024}
1025
1026impl Name for Dist {
1027    fn name(&self) -> &PackageName {
1028        match self {
1029            Self::Built(dist) => dist.name(),
1030            Self::Source(dist) => dist.name(),
1031        }
1032    }
1033}
1034
1035impl Name for CompatibleDist<'_> {
1036    fn name(&self) -> &PackageName {
1037        match self {
1038            Self::InstalledDist(dist) => dist.name(),
1039            Self::SourceDist {
1040                sdist,
1041                prioritized: _,
1042            } => sdist.name(),
1043            Self::CompatibleWheel {
1044                wheel,
1045                priority: _,
1046                prioritized: _,
1047            } => wheel.name(),
1048            Self::IncompatibleWheel {
1049                sdist,
1050                wheel: _,
1051                prioritized: _,
1052            } => sdist.name(),
1053        }
1054    }
1055}
1056
1057impl DistributionMetadata for RegistryBuiltWheel {
1058    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1059        VersionOrUrlRef::Version(&self.filename.version)
1060    }
1061}
1062
1063impl DistributionMetadata for RegistryBuiltDist {
1064    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1065        self.best_wheel().version_or_url()
1066    }
1067}
1068
1069impl DistributionMetadata for DirectUrlBuiltDist {
1070    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1071        VersionOrUrlRef::Url(&self.url)
1072    }
1073
1074    fn version_id(&self) -> VersionId {
1075        VersionId::from_archive(self.location.as_ref().clone(), None)
1076    }
1077}
1078
1079impl DistributionMetadata for PathBuiltDist {
1080    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1081        VersionOrUrlRef::Url(&self.url)
1082    }
1083
1084    fn version_id(&self) -> VersionId {
1085        VersionId::from_path(self.install_path.as_ref())
1086    }
1087}
1088
1089impl DistributionMetadata for GitPathBuiltDist {
1090    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1091        VersionOrUrlRef::Url(&self.url)
1092    }
1093}
1094
1095impl DistributionMetadata for RegistrySourceDist {
1096    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1097        VersionOrUrlRef::Version(&self.version)
1098    }
1099}
1100
1101impl DistributionMetadata for DirectUrlSourceDist {
1102    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1103        VersionOrUrlRef::Url(&self.url)
1104    }
1105
1106    fn version_id(&self) -> VersionId {
1107        VersionId::from_archive(
1108            self.location.as_ref().clone(),
1109            self.subdirectory.clone().map(Path::into_path_buf),
1110        )
1111    }
1112}
1113
1114impl DistributionMetadata for GitPathSourceDist {
1115    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1116        VersionOrUrlRef::Url(&self.url)
1117    }
1118
1119    fn version_id(&self) -> VersionId {
1120        VersionId::from_git(self.git.as_ref(), Some(&self.install_path))
1121    }
1122}
1123
1124impl DistributionMetadata for GitDirectorySourceDist {
1125    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1126        VersionOrUrlRef::Url(&self.url)
1127    }
1128
1129    fn version_id(&self) -> VersionId {
1130        VersionId::from_git(self.git.as_ref(), self.subdirectory.as_deref())
1131    }
1132}
1133
1134impl DistributionMetadata for PathSourceDist {
1135    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1136        VersionOrUrlRef::Url(&self.url)
1137    }
1138
1139    fn version_id(&self) -> VersionId {
1140        VersionId::from_path(self.install_path.as_ref())
1141    }
1142}
1143
1144impl DistributionMetadata for DirectorySourceDist {
1145    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1146        VersionOrUrlRef::Url(&self.url)
1147    }
1148
1149    fn version_id(&self) -> VersionId {
1150        VersionId::from_directory(self.install_path.as_ref())
1151    }
1152}
1153
1154impl DistributionMetadata for SourceDist {
1155    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1156        match self {
1157            Self::Registry(dist) => dist.version_or_url(),
1158            Self::DirectUrl(dist) => dist.version_or_url(),
1159            Self::GitPath(dist) => dist.version_or_url(),
1160            Self::GitDirectory(dist) => dist.version_or_url(),
1161            Self::Path(dist) => dist.version_or_url(),
1162            Self::Directory(dist) => dist.version_or_url(),
1163        }
1164    }
1165
1166    fn version_id(&self) -> VersionId {
1167        match self {
1168            Self::Registry(dist) => dist.version_id(),
1169            Self::DirectUrl(dist) => dist.version_id(),
1170            Self::GitPath(dist) => dist.version_id(),
1171            Self::GitDirectory(dist) => dist.version_id(),
1172            Self::Path(dist) => dist.version_id(),
1173            Self::Directory(dist) => dist.version_id(),
1174        }
1175    }
1176}
1177
1178impl DistributionMetadata for BuiltDist {
1179    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1180        match self {
1181            Self::Registry(dist) => dist.version_or_url(),
1182            Self::DirectUrl(dist) => dist.version_or_url(),
1183            Self::Path(dist) => dist.version_or_url(),
1184            Self::GitPath(dist) => dist.version_or_url(),
1185        }
1186    }
1187
1188    fn version_id(&self) -> VersionId {
1189        match self {
1190            Self::Registry(dist) => dist.version_id(),
1191            Self::DirectUrl(dist) => dist.version_id(),
1192            Self::Path(dist) => dist.version_id(),
1193            Self::GitPath(dist) => dist.version_id(),
1194        }
1195    }
1196}
1197
1198impl DistributionMetadata for Dist {
1199    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1200        match self {
1201            Self::Built(dist) => dist.version_or_url(),
1202            Self::Source(dist) => dist.version_or_url(),
1203        }
1204    }
1205
1206    fn version_id(&self) -> VersionId {
1207        match self {
1208            Self::Built(dist) => dist.version_id(),
1209            Self::Source(dist) => dist.version_id(),
1210        }
1211    }
1212}
1213
1214impl RemoteSource for File {
1215    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1216        Ok(Cow::Borrowed(&self.filename))
1217    }
1218
1219    fn size(&self) -> Option<u64> {
1220        self.size
1221    }
1222}
1223
1224impl RemoteSource for Url {
1225    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1226        // Identify the last segment of the URL as the filename.
1227        let mut path_segments = self.path_segments().ok_or_else(|| {
1228            Error::MissingPathSegments(DisplaySafeUrl::ref_cast(self).to_string())
1229        })?;
1230
1231        // This is guaranteed by the contract of `Url::path_segments`.
1232        let last = path_segments
1233            .next_back()
1234            .expect("path segments is non-empty");
1235
1236        // Decode the filename, which may be percent-encoded.
1237        let filename = percent_encoding::percent_decode_str(last).decode_utf8()?;
1238
1239        Ok(filename)
1240    }
1241
1242    fn size(&self) -> Option<u64> {
1243        None
1244    }
1245}
1246
1247impl RemoteSource for UrlString {
1248    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1249        let url = self.as_ref();
1250        if memchr3(b'?', b'#', b'%', url.as_bytes()).is_none()
1251            && let Some((_, filename)) = url.rsplit_once('/')
1252        {
1253            return Ok(Cow::Borrowed(filename));
1254        }
1255
1256        // Take the last segment, stripping any query or fragment.
1257        let last = self
1258            .base_str()
1259            .split('/')
1260            .next_back()
1261            .ok_or_else(|| Error::MissingPathSegments(self.to_string()))?;
1262
1263        // Decode the filename, which may be percent-encoded.
1264        let filename = percent_encoding::percent_decode_str(last).decode_utf8()?;
1265
1266        Ok(filename)
1267    }
1268
1269    fn size(&self) -> Option<u64> {
1270        None
1271    }
1272}
1273
1274impl RemoteSource for RegistryBuiltWheel {
1275    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1276        self.file.filename()
1277    }
1278
1279    fn size(&self) -> Option<u64> {
1280        self.file.size()
1281    }
1282}
1283
1284impl RemoteSource for RegistryBuiltDist {
1285    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1286        self.best_wheel().filename()
1287    }
1288
1289    fn size(&self) -> Option<u64> {
1290        self.best_wheel().size()
1291    }
1292}
1293
1294impl RemoteSource for RegistrySourceDist {
1295    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1296        self.file.filename()
1297    }
1298
1299    fn size(&self) -> Option<u64> {
1300        self.file.size()
1301    }
1302}
1303
1304impl RemoteSource for DirectUrlBuiltDist {
1305    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1306        self.url.filename()
1307    }
1308
1309    fn size(&self) -> Option<u64> {
1310        self.size
1311    }
1312}
1313
1314impl RemoteSource for DirectUrlSourceDist {
1315    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1316        self.url.filename()
1317    }
1318
1319    fn size(&self) -> Option<u64> {
1320        self.size
1321    }
1322}
1323
1324impl RemoteSource for GitPathSourceDist {
1325    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1326        // The filename is the last segment of the URL, before any `@`.
1327        match self.url.filename()? {
1328            Cow::Borrowed(filename) if let Some((_, suffix)) = filename.rsplit_once('@') => {
1329                Ok(Cow::Borrowed(suffix))
1330            }
1331            Cow::Owned(ref filename) if let Some((_, suffix)) = filename.rsplit_once('@') => {
1332                Ok(Cow::Owned(suffix.to_owned()))
1333            }
1334            filename => Ok(filename),
1335        }
1336    }
1337
1338    fn size(&self) -> Option<u64> {
1339        self.url.size()
1340    }
1341}
1342
1343impl RemoteSource for GitDirectorySourceDist {
1344    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1345        // The filename is the last segment of the URL, before any `@`.
1346        match self.url.filename()? {
1347            Cow::Borrowed(filename) if let Some((_, suffix)) = filename.rsplit_once('@') => {
1348                Ok(Cow::Borrowed(suffix))
1349            }
1350            Cow::Owned(ref filename) if let Some((_, suffix)) = filename.rsplit_once('@') => {
1351                Ok(Cow::Owned(suffix.to_owned()))
1352            }
1353            filename => Ok(filename),
1354        }
1355    }
1356
1357    fn size(&self) -> Option<u64> {
1358        self.url.size()
1359    }
1360}
1361
1362impl RemoteSource for PathBuiltDist {
1363    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1364        self.url.filename()
1365    }
1366
1367    fn size(&self) -> Option<u64> {
1368        self.url.size()
1369    }
1370}
1371
1372impl RemoteSource for GitPathBuiltDist {
1373    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1374        self.url.filename()
1375    }
1376
1377    fn size(&self) -> Option<u64> {
1378        self.url.size()
1379    }
1380}
1381
1382impl RemoteSource for PathSourceDist {
1383    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1384        self.url.filename()
1385    }
1386
1387    fn size(&self) -> Option<u64> {
1388        self.url.size()
1389    }
1390}
1391
1392impl RemoteSource for DirectorySourceDist {
1393    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1394        self.url.filename()
1395    }
1396
1397    fn size(&self) -> Option<u64> {
1398        self.url.size()
1399    }
1400}
1401
1402impl RemoteSource for SourceDist {
1403    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1404        match self {
1405            Self::Registry(dist) => dist.filename(),
1406            Self::DirectUrl(dist) => dist.filename(),
1407            Self::GitPath(dist) => dist.filename(),
1408            Self::GitDirectory(dist) => dist.filename(),
1409            Self::Path(dist) => dist.filename(),
1410            Self::Directory(dist) => dist.filename(),
1411        }
1412    }
1413
1414    fn size(&self) -> Option<u64> {
1415        match self {
1416            Self::Registry(dist) => dist.size(),
1417            Self::DirectUrl(dist) => dist.size(),
1418            Self::GitPath(dist) => dist.size(),
1419            Self::GitDirectory(dist) => dist.size(),
1420            Self::Path(dist) => dist.size(),
1421            Self::Directory(dist) => dist.size(),
1422        }
1423    }
1424}
1425
1426impl RemoteSource for BuiltDist {
1427    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1428        match self {
1429            Self::Registry(dist) => dist.filename(),
1430            Self::DirectUrl(dist) => dist.filename(),
1431            Self::Path(dist) => dist.filename(),
1432            Self::GitPath(dist) => dist.filename(),
1433        }
1434    }
1435
1436    fn size(&self) -> Option<u64> {
1437        match self {
1438            Self::Registry(dist) => dist.size(),
1439            Self::DirectUrl(dist) => dist.size(),
1440            Self::Path(dist) => dist.size(),
1441            Self::GitPath(dist) => dist.size(),
1442        }
1443    }
1444}
1445
1446impl RemoteSource for Dist {
1447    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1448        match self {
1449            Self::Built(dist) => dist.filename(),
1450            Self::Source(dist) => dist.filename(),
1451        }
1452    }
1453
1454    fn size(&self) -> Option<u64> {
1455        match self {
1456            Self::Built(dist) => dist.size(),
1457            Self::Source(dist) => dist.size(),
1458        }
1459    }
1460}
1461
1462impl Identifier for DisplaySafeUrl {
1463    fn distribution_id(&self) -> DistributionId {
1464        DistributionId::Url(uv_cache_key::CanonicalUrl::new(self.clone()))
1465    }
1466
1467    fn resource_id(&self) -> ResourceId {
1468        ResourceId::Url(uv_cache_key::RepositoryUrl::new(self.clone()))
1469    }
1470}
1471
1472impl Identifier for File {
1473    fn distribution_id(&self) -> DistributionId {
1474        self.hashes
1475            .first()
1476            .cloned()
1477            .map(DistributionId::Digest)
1478            .unwrap_or_else(|| self.url.distribution_id())
1479    }
1480
1481    fn resource_id(&self) -> ResourceId {
1482        self.hashes
1483            .first()
1484            .cloned()
1485            .map(ResourceId::Digest)
1486            .unwrap_or_else(|| self.url.resource_id())
1487    }
1488}
1489
1490impl Identifier for Path {
1491    fn distribution_id(&self) -> DistributionId {
1492        DistributionId::PathBuf(self.to_path_buf())
1493    }
1494
1495    fn resource_id(&self) -> ResourceId {
1496        ResourceId::PathBuf(self.to_path_buf())
1497    }
1498}
1499
1500impl Identifier for FileLocation {
1501    fn distribution_id(&self) -> DistributionId {
1502        match self {
1503            Self::RelativeUrl(base, url) => {
1504                DistributionId::RelativeUrl(base.to_string(), url.to_string())
1505            }
1506            Self::AbsoluteUrl(url) => DistributionId::AbsoluteUrl(url.to_string()),
1507        }
1508    }
1509
1510    fn resource_id(&self) -> ResourceId {
1511        match self {
1512            Self::RelativeUrl(base, url) => {
1513                ResourceId::RelativeUrl(base.to_string(), url.to_string())
1514            }
1515            Self::AbsoluteUrl(url) => ResourceId::AbsoluteUrl(url.to_string()),
1516        }
1517    }
1518}
1519
1520impl Identifier for RegistryBuiltWheel {
1521    fn distribution_id(&self) -> DistributionId {
1522        self.file.distribution_id()
1523    }
1524
1525    fn resource_id(&self) -> ResourceId {
1526        self.file.resource_id()
1527    }
1528}
1529
1530impl Identifier for RegistryBuiltDist {
1531    fn distribution_id(&self) -> DistributionId {
1532        self.best_wheel().distribution_id()
1533    }
1534
1535    fn resource_id(&self) -> ResourceId {
1536        self.best_wheel().resource_id()
1537    }
1538}
1539
1540impl Identifier for RegistrySourceDist {
1541    fn distribution_id(&self) -> DistributionId {
1542        self.file.distribution_id()
1543    }
1544
1545    fn resource_id(&self) -> ResourceId {
1546        self.file.resource_id()
1547    }
1548}
1549
1550impl Identifier for DirectUrlBuiltDist {
1551    fn distribution_id(&self) -> DistributionId {
1552        self.url.distribution_id()
1553    }
1554
1555    fn resource_id(&self) -> ResourceId {
1556        self.url.resource_id()
1557    }
1558}
1559
1560impl Identifier for DirectUrlSourceDist {
1561    fn distribution_id(&self) -> DistributionId {
1562        self.url.distribution_id()
1563    }
1564
1565    fn resource_id(&self) -> ResourceId {
1566        self.url.resource_id()
1567    }
1568}
1569
1570impl Identifier for PathBuiltDist {
1571    fn distribution_id(&self) -> DistributionId {
1572        self.url.distribution_id()
1573    }
1574
1575    fn resource_id(&self) -> ResourceId {
1576        self.url.resource_id()
1577    }
1578}
1579
1580impl Identifier for GitPathBuiltDist {
1581    fn distribution_id(&self) -> DistributionId {
1582        self.url.distribution_id()
1583    }
1584
1585    fn resource_id(&self) -> ResourceId {
1586        self.url.resource_id()
1587    }
1588}
1589
1590impl Identifier for PathSourceDist {
1591    fn distribution_id(&self) -> DistributionId {
1592        self.url.distribution_id()
1593    }
1594
1595    fn resource_id(&self) -> ResourceId {
1596        self.url.resource_id()
1597    }
1598}
1599
1600impl Identifier for DirectorySourceDist {
1601    fn distribution_id(&self) -> DistributionId {
1602        self.url.distribution_id()
1603    }
1604
1605    fn resource_id(&self) -> ResourceId {
1606        self.url.resource_id()
1607    }
1608}
1609
1610impl Identifier for GitPathSourceDist {
1611    fn distribution_id(&self) -> DistributionId {
1612        self.url.distribution_id()
1613    }
1614
1615    fn resource_id(&self) -> ResourceId {
1616        self.url.resource_id()
1617    }
1618}
1619
1620impl Identifier for GitDirectorySourceDist {
1621    fn distribution_id(&self) -> DistributionId {
1622        self.url.distribution_id()
1623    }
1624
1625    fn resource_id(&self) -> ResourceId {
1626        self.url.resource_id()
1627    }
1628}
1629
1630impl Identifier for SourceDist {
1631    fn distribution_id(&self) -> DistributionId {
1632        match self {
1633            Self::Registry(dist) => dist.distribution_id(),
1634            Self::DirectUrl(dist) => dist.distribution_id(),
1635            Self::GitPath(dist) => dist.distribution_id(),
1636            Self::GitDirectory(dist) => dist.distribution_id(),
1637            Self::Path(dist) => dist.distribution_id(),
1638            Self::Directory(dist) => dist.distribution_id(),
1639        }
1640    }
1641
1642    fn resource_id(&self) -> ResourceId {
1643        match self {
1644            Self::Registry(dist) => dist.resource_id(),
1645            Self::DirectUrl(dist) => dist.resource_id(),
1646            Self::GitPath(dist) => dist.resource_id(),
1647            Self::GitDirectory(dist) => dist.resource_id(),
1648            Self::Path(dist) => dist.resource_id(),
1649            Self::Directory(dist) => dist.resource_id(),
1650        }
1651    }
1652}
1653
1654impl Identifier for BuiltDist {
1655    fn distribution_id(&self) -> DistributionId {
1656        match self {
1657            Self::Registry(dist) => dist.distribution_id(),
1658            Self::DirectUrl(dist) => dist.distribution_id(),
1659            Self::Path(dist) => dist.distribution_id(),
1660            Self::GitPath(dist) => dist.distribution_id(),
1661        }
1662    }
1663
1664    fn resource_id(&self) -> ResourceId {
1665        match self {
1666            Self::Registry(dist) => dist.resource_id(),
1667            Self::DirectUrl(dist) => dist.resource_id(),
1668            Self::Path(dist) => dist.resource_id(),
1669            Self::GitPath(dist) => dist.resource_id(),
1670        }
1671    }
1672}
1673
1674impl Identifier for InstalledDist {
1675    fn distribution_id(&self) -> DistributionId {
1676        self.install_path().distribution_id()
1677    }
1678
1679    fn resource_id(&self) -> ResourceId {
1680        self.install_path().resource_id()
1681    }
1682}
1683
1684impl Identifier for Dist {
1685    fn distribution_id(&self) -> DistributionId {
1686        match self {
1687            Self::Built(dist) => dist.distribution_id(),
1688            Self::Source(dist) => dist.distribution_id(),
1689        }
1690    }
1691
1692    fn resource_id(&self) -> ResourceId {
1693        match self {
1694            Self::Built(dist) => dist.resource_id(),
1695            Self::Source(dist) => dist.resource_id(),
1696        }
1697    }
1698}
1699
1700impl Identifier for DirectSourceUrl<'_> {
1701    fn distribution_id(&self) -> DistributionId {
1702        self.url.distribution_id()
1703    }
1704
1705    fn resource_id(&self) -> ResourceId {
1706        self.url.resource_id()
1707    }
1708}
1709
1710impl Identifier for GitDirectorySourceUrl<'_> {
1711    fn distribution_id(&self) -> DistributionId {
1712        self.url.distribution_id()
1713    }
1714
1715    fn resource_id(&self) -> ResourceId {
1716        self.url.resource_id()
1717    }
1718}
1719
1720impl Identifier for GitPathSourceUrl<'_> {
1721    fn distribution_id(&self) -> DistributionId {
1722        self.url.distribution_id()
1723    }
1724
1725    fn resource_id(&self) -> ResourceId {
1726        self.url.resource_id()
1727    }
1728}
1729
1730impl Identifier for PathSourceUrl<'_> {
1731    fn distribution_id(&self) -> DistributionId {
1732        self.url.distribution_id()
1733    }
1734
1735    fn resource_id(&self) -> ResourceId {
1736        self.url.resource_id()
1737    }
1738}
1739
1740impl Identifier for DirectorySourceUrl<'_> {
1741    fn distribution_id(&self) -> DistributionId {
1742        self.url.distribution_id()
1743    }
1744
1745    fn resource_id(&self) -> ResourceId {
1746        self.url.resource_id()
1747    }
1748}
1749
1750impl Identifier for SourceUrl<'_> {
1751    fn distribution_id(&self) -> DistributionId {
1752        match self {
1753            Self::Direct(url) => url.distribution_id(),
1754            Self::GitDirectory(url) => url.distribution_id(),
1755            Self::GitPath(url) => url.distribution_id(),
1756            Self::Path(url) => url.distribution_id(),
1757            Self::Directory(url) => url.distribution_id(),
1758        }
1759    }
1760
1761    fn resource_id(&self) -> ResourceId {
1762        match self {
1763            Self::Direct(url) => url.resource_id(),
1764            Self::GitDirectory(url) => url.resource_id(),
1765            Self::GitPath(url) => url.resource_id(),
1766            Self::Path(url) => url.resource_id(),
1767            Self::Directory(url) => url.resource_id(),
1768        }
1769    }
1770}
1771
1772impl Identifier for BuildableSource<'_> {
1773    fn distribution_id(&self) -> DistributionId {
1774        match self {
1775            Self::Dist(source) => source.distribution_id(),
1776            Self::Url(source) => source.distribution_id(),
1777        }
1778    }
1779
1780    fn resource_id(&self) -> ResourceId {
1781        match self {
1782            Self::Dist(source) => source.resource_id(),
1783            Self::Url(source) => source.resource_id(),
1784        }
1785    }
1786}
1787
1788#[cfg(test)]
1789mod test {
1790    use crate::{BuiltDist, Dist, RemoteSource, SourceDist, UrlString};
1791    use uv_redacted::DisplaySafeUrl;
1792
1793    /// Ensure that we don't accidentally grow the `Dist` sizes.
1794    #[test]
1795    fn dist_size() {
1796        assert!(size_of::<Dist>() <= 200, "{}", size_of::<Dist>());
1797        assert!(size_of::<BuiltDist>() <= 200, "{}", size_of::<BuiltDist>());
1798        assert!(
1799            size_of::<SourceDist>() <= 176,
1800            "{}",
1801            size_of::<SourceDist>()
1802        );
1803    }
1804
1805    #[test]
1806    fn remote_source() {
1807        for url in [
1808            "https://example.com/foo-0.1.0.tar.gz",
1809            "https://example.com/foo%2D0.1.0.tar.gz",
1810            "https://example.com/foo-0.1.0.tar.gz#fragment",
1811            "https://example.com/foo-0.1.0.tar.gz?query",
1812            "https://example.com/foo-0.1.0.tar.gz?query#fragment",
1813            "https://example.com/foo-0.1.0.tar.gz#fragment?query",
1814            "https://example.com/foo-0.1.0.tar.gz#fragment/3?query",
1815            "https://example.com/foo%2D0.1.0.tar.gz#fragment/3?query",
1816            "https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment",
1817            "https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment/3",
1818            "https://example.com/foo%2D0.1.0.tar.gz?query=1/2#fragment/3",
1819        ] {
1820            let url = DisplaySafeUrl::parse(url).unwrap();
1821            assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}");
1822            let url = UrlString::from(url.clone());
1823            assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}");
1824        }
1825    }
1826
1827    #[test]
1828    fn remote_source_redacts_missing_path_segments() {
1829        for (input, expected) in [
1830            (
1831                "mailto:ferris@example.com?X-Amz-Signature=sentinel",
1832                "mailto:ferris@example.com?X-Amz-Signature=****",
1833            ),
1834            (
1835                "ssh://user:secret@example.com?sig=sentinel",
1836                "ssh://user:****@example.com?sig=****",
1837            ),
1838            (
1839                "mailto:ferris@example.com?sig=one&X-Amz-Credential=two&X-Amz-Security-Token=three&X-Amz-Signature=four&token=kept#fragment",
1840                "mailto:ferris@example.com?sig=****&X-Amz-Credential=****&X-Amz-Security-Token=****&X-Amz-Signature=****&token=kept#fragment",
1841            ),
1842            (
1843                "mailto:ferris@example.com?x-amz%2dsignature=sentinel&safe=value",
1844                "mailto:ferris@example.com?x-amz-signature=****&safe=value",
1845            ),
1846            (
1847                "mailto:ferris@example.com?token=kept#fragment",
1848                "mailto:ferris@example.com?token=kept#fragment",
1849            ),
1850        ] {
1851            let url = url::Url::parse(input).unwrap();
1852            let error = RemoteSource::filename(&url).unwrap_err();
1853            let crate::Error::MissingPathSegments(payload) = &error else {
1854                panic!("expected missing path segments");
1855            };
1856            assert_eq!(payload, expected);
1857            assert_eq!(
1858                error.to_string(),
1859                format!("Could not extract path segments from URL: {expected}")
1860            );
1861            assert_eq!(
1862                format!("{error:?}"),
1863                format!("MissingPathSegments({expected:?})")
1864            );
1865            assert!(std::error::Error::source(&error).is_none());
1866            assert_eq!(url.as_str(), input);
1867        }
1868
1869        let input = "https://example.org/demo%20name.whl?sig=sentinel";
1870        let url = url::Url::parse(input).unwrap();
1871        assert_eq!(RemoteSource::filename(&url).unwrap(), "demo name.whl");
1872        assert_eq!(url.as_str(), input);
1873    }
1874}