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