Skip to main content

uv_distribution_types/
lib.rs

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