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