#
cargo/core/
package_id.rs

    1use std::collections::HashSet;
    2use std::fmt::{self, Formatter};
    3use std::hash;
    4use std::hash::Hash;
    5use std::path::Path;
    6use std::ptr;
    7use std::sync::Mutex;
    8use std::sync::OnceLock;
    9
    10use serde::de;
    11use serde::ser;
    12
    13use crate::core::PackageIdSpec;
    14use crate::core::SourceId;
    15use crate::util::CargoResult;
    16use crate::util::interning::InternedString;
    17
    18static PACKAGE_ID_CACHE: OnceLock<Mutex<HashSet<&'static PackageIdInner>>> = OnceLock::new();
    19
    20/// Identifier for a specific version of a package in a specific source.
    21#[derive(Clone, Copy, Eq, PartialOrd, Ord)]
    22pub struct PackageId {
    23    inner: &'static PackageIdInner,
    24}
    25
    26#[derive(PartialOrd, Eq, Ord)]
    27struct PackageIdInner {
    28    name: InternedString,
    29    version: semver::Version,
    30    source_id: SourceId,
    31}
    32
    33// Custom equality that uses full equality of SourceId, rather than its custom equality.
    34//
    35// The `build` part of the version is usually ignored (like a "comment").
    36// However, there are some cases where it is important. The download path from
    37// a registry includes the build metadata, and Cargo uses PackageIds for
    38// creating download paths. Including it here prevents the PackageId interner
    39// from getting poisoned with PackageIds where that build metadata is missing.
    40impl PartialEq for PackageIdInner {
    41    fn eq(&self, other: &Self) -> bool {
    42        self.name == other.name
    43            && self.version == other.version
    44            && self.source_id.full_eq(other.source_id)
    45    }
    46}
    47
    48// Custom hash that is coherent with the custom equality above.
    49impl Hash for PackageIdInner {
    50    fn hash<S: hash::Hasher>(&self, into: &mut S) {
    51        self.name.hash(into);
    52        self.version.hash(into);
    53        self.source_id.full_hash(into);
    54    }
    55}
    56
    57impl ser::Serialize for PackageId {
    58    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    59    where
    60        S: ser::Serializer,
    61    {
    62        s.collect_str(&format_args!(
    63            "{} {} ({})",
    64            self.inner.name,
    65            self.inner.version,
    66            self.inner.source_id.as_url()
    67        ))
    68    }
    69}
    70
    71impl<'de> de::Deserialize<'de> for PackageId {
    72    fn deserialize<D>(d: D) -> Result<PackageId, D::Error>
    73    where
    74        D: de::Deserializer<'de>,
    75    {
    76        let string = String::deserialize(d)?;
    77
    78        let (field, rest) = string
    79            .split_once(' ')
    80            .ok_or_else(|| de::Error::custom("invalid serialized PackageId"))?;
    81        let name = field.into();
    82
    83        let (field, rest) = rest
    84            .split_once(' ')
    85            .ok_or_else(|| de::Error::custom("invalid serialized PackageId"))?;
    86        let version = field.parse().map_err(de::Error::custom)?;
    87
    88        let url =
    89            strip_parens(rest).ok_or_else(|| de::Error::custom("invalid serialized PackageId"))?;
    90        let source_id = SourceId::from_url(url).map_err(de::Error::custom)?;
    91
    92        Ok(PackageId::new(name, version, source_id))
    93    }
    94}
    95
    96fn strip_parens(value: &str) -> Option<&str> {
    97    let value = value.strip_prefix('(')?;
    98    let value = value.strip_suffix(')')?;
    99    Some(value)
    100}
    101
    102impl PartialEq for PackageId {
    103    fn eq(&self, other: &PackageId) -> bool {
    104        if ptr::eq(self.inner, other.inner) {
    105            return true;
    106        }
    107        // This is here so that PackageId uses SourceId's and Version's idea
    108        // of equality. PackageIdInner uses a more exact notion of equality.
    109        self.inner.name == other.inner.name
    110            && self.inner.version == other.inner.version
    111            && self.inner.source_id == other.inner.source_id
    112    }
    113}
    114
    115impl Hash for PackageId {
    116    fn hash<S: hash::Hasher>(&self, state: &mut S) {
    117        // This is here (instead of derived) so that PackageId uses SourceId's
    118        // and Version's idea of equality. PackageIdInner uses a more exact
    119        // notion of hashing.
    120        self.inner.name.hash(state);
    121        self.inner.version.hash(state);
    122        self.inner.source_id.hash(state);
    123    }
    124}
    125
    126impl PackageId {
    127    pub fn try_new(
    128        name: impl Into<InternedString>,
    129        version: &str,
    130        sid: SourceId,
    131    ) -> CargoResult<PackageId> {
    132        let v = version.parse()?;
    133        Ok(PackageId::new(name.into(), v, sid))
    134    }
    135
    136    pub fn new(name: InternedString, version: semver::Version, source_id: SourceId) -> PackageId {
    137        let inner = PackageIdInner {
    138            name,
    139            version,
    140            source_id,
    141        };
    142        let mut cache = PACKAGE_ID_CACHE
    143            .get_or_init(|| Default::default())
    144            .lock()
    145            .unwrap();
    146        let inner = cache.get(&inner).cloned().unwrap_or_else(|| {
    147            let inner = Box::leak(Box::new(inner));
    148            cache.insert(inner);
    149            inner
    150        });
    151        PackageId { inner }
    152    }
    153
    154    pub fn name(self) -> InternedString {
    155        self.inner.name
    156    }
    157    pub fn version(self) -> &'static semver::Version {
    158        &self.inner.version
    159    }
    160    pub fn source_id(self) -> SourceId {
    161        self.inner.source_id
    162    }
    163
    164    pub fn with_source_id(self, source: SourceId) -> PackageId {
    165        PackageId::new(self.inner.name, self.inner.version.clone(), source)
    166    }
    167
    168    pub fn map_source(self, to_replace: SourceId, replace_with: SourceId) -> Self {
    169        if self.source_id() == to_replace {
    170            self.with_source_id(replace_with)
    171        } else {
    172            self
    173        }
    174    }
    175
    176    /// Returns a value that implements a "stable" hashable value.
    177    ///
    178    /// Stable hashing removes the path prefix of the workspace from path
    179    /// packages. This helps with reproducible builds, since this hash is part
    180    /// of the symbol metadata, and we don't want the absolute path where the
    181    /// build is performed to affect the binary output.
    182    pub fn stable_hash(self, workspace: &Path) -> PackageIdStableHash<'_> {
    183        PackageIdStableHash(self, workspace)
    184    }
    185
    186    /// Filename of the `.crate` tarball, e.g., `once_cell-1.18.0.crate`.
    187    pub fn tarball_name(&self) -> String {
    188        format!("{}-{}.crate", self.name(), self.version())
    189    }
    190
    191    /// Convert a `PackageId` to a `PackageIdSpec`, which will have both the `PartialVersion` and `Url`
    192    /// fields filled in.
    193    pub fn to_spec(&self) -> PackageIdSpec {
    194        PackageIdSpec::new(String::from(self.name().as_str()))
    195            .with_version(self.version().clone().into())
    196            .with_url(self.source_id().url().clone())
    197            .with_kind(self.source_id().kind().clone())
    198    }
    199}
    200
    201pub struct PackageIdStableHash<'a>(PackageId, &'a Path);
    202
    203impl<'a> Hash for PackageIdStableHash<'a> {
    204    fn hash<S: hash::Hasher>(&self, state: &mut S) {
    205        self.0.inner.name.hash(state);
    206        self.0.inner.version.hash(state);
    207        self.0.inner.source_id.stable_hash(self.1, state);
    208    }
    209}
    210
    211impl fmt::Display for PackageId {
    212    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    213        write!(f, "{} v{}", self.inner.name, self.inner.version)?;
    214
    215        if !self.inner.source_id.is_crates_io() {
    216            write!(f, " ({})", self.inner.source_id)?;
    217        }
    218
    219        Ok(())
    220    }
    221}
    222
    223impl fmt::Debug for PackageId {
    224    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    225        f.debug_struct("PackageId")
    226            .field("name", &self.inner.name)
    227            .field("version", &self.inner.version.to_string())
    228            .field("source", &self.inner.source_id.to_string())
    229            .finish()
    230    }
    231}
    232
    233impl fmt::Debug for PackageIdInner {
    234    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    235        f.debug_struct("PackageIdInner")
    236            .field("name", &self.name)
    237            .field("version", &self.version.to_string())
    238            .field("source", &self.source_id.to_string())
    239            .finish()
    240    }
    241}
    242
    243#[cfg(test)]
    244mod tests {
    245    use super::PackageId;
    246    use crate::core::SourceId;
    247    use crate::sources::CRATES_IO_INDEX;
    248    use crate::util::IntoUrl;
    249
    250    #[test]
    251    fn invalid_version_handled_nicely() {
    252        let loc = CRATES_IO_INDEX.into_url().unwrap();
    253        let repo = SourceId::for_registry(&loc).unwrap();
    254
    255        assert!(PackageId::try_new("foo", "1.0", repo).is_err());
    256        assert!(PackageId::try_new("foo", "1", repo).is_err());
    257        assert!(PackageId::try_new("foo", "bar", repo).is_err());
    258        assert!(PackageId::try_new("foo", "", repo).is_err());
    259    }
    260
    261    #[test]
    262    fn display() {
    263        let loc = CRATES_IO_INDEX.into_url().unwrap();
    264        let pkg_id =
    265            PackageId::try_new("foo", "1.0.0", SourceId::for_registry(&loc).unwrap()).unwrap();
    266        assert_eq!("foo v1.0.0", pkg_id.to_string());
    267    }
    268
    269    #[test]
    270    fn unequal_build_metadata() {
    271        let loc = CRATES_IO_INDEX.into_url().unwrap();
    272        let repo = SourceId::for_registry(&loc).unwrap();
    273        let first = PackageId::try_new("foo", "0.0.1+first", repo).unwrap();
    274        let second = PackageId::try_new("foo", "0.0.1+second", repo).unwrap();
    275        assert_ne!(first, second);
    276        assert_ne!(first.inner, second.inner);
    277    }
    278}