Skip to main content

typst_pack/
identity.rs

1//! Shared canonical identity values.
2
3use std::hash::{Hash, Hasher};
4
5/// The semantic role of a [`CanonicalIdentity`].
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub enum CanonicalIdentityRole {
8    Pack,
9    PackageTree,
10    FontContainer,
11    Compilation,
12    CompilationResult,
13}
14
15impl CanonicalIdentityRole {
16    /// The stable role string used by serialized identity declarations.
17    pub const fn as_str(self) -> &'static str {
18        match self {
19            Self::Pack => "pack",
20            Self::PackageTree => "complete-package-tree",
21            Self::FontContainer => "font-container",
22            Self::Compilation => "compilation",
23            Self::CompilationResult => "compilation-result",
24        }
25    }
26
27    pub(crate) const fn schema(self) -> &'static str {
28        match self {
29            Self::Pack => "typst-pack-identity-v1",
30            Self::PackageTree => "typst-pack-complete-package-tree-v1",
31            Self::FontContainer => "typst-pack-font-container-identity-v1",
32            Self::Compilation => "typst-pack-compilation-v1",
33            Self::CompilationResult => "typst-pack-compilation-result-v1",
34        }
35    }
36}
37
38/// A role-separated canonical semantic identity.
39///
40/// Equality includes the role, schema, algorithm, and digest.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
42pub struct CanonicalIdentity {
43    role: CanonicalIdentityRole,
44    digest: u128,
45}
46
47impl Hash for CanonicalIdentity {
48    fn hash<H: Hasher>(&self, state: &mut H) {
49        // Schema-v1 projections hashed the digest-only identity newtypes.
50        self.digest.hash(state);
51    }
52}
53
54impl CanonicalIdentity {
55    pub(crate) const fn from_digest(role: CanonicalIdentityRole, digest: u128) -> Self {
56        Self { role, digest }
57    }
58
59    /// Derives the Font Container identity of exact container bytes.
60    pub fn for_font_container_bytes(data: &[u8]) -> Self {
61        Self::from_digest(
62            CanonicalIdentityRole::FontContainer,
63            typst::utils::hash128(&data),
64        )
65    }
66
67    /// The semantic role separated by this identity.
68    pub const fn role(self) -> CanonicalIdentityRole {
69        self.role
70    }
71
72    /// The identity schema used by this release.
73    pub const fn schema(self) -> &'static str {
74        self.role.schema()
75    }
76
77    /// The deterministic digest algorithm used by the schema.
78    pub const fn algorithm(self) -> &'static str {
79        "typst-hash128-0.15"
80    }
81
82    /// The deterministic 128-bit digest in big-endian order.
83    pub const fn digest(self) -> [u8; 16] {
84        self.digest.to_be_bytes()
85    }
86
87    pub(crate) const fn digest_value(self) -> u128 {
88        self.digest
89    }
90
91    pub(crate) fn encode(self) -> String {
92        format!("{:032x}", self.digest)
93    }
94
95    pub(crate) fn decode(role: CanonicalIdentityRole, value: &str) -> Option<Self> {
96        (value.len() == 32)
97            .then(|| u128::from_str_radix(value, 16).ok())
98            .flatten()
99            .map(|digest| Self::from_digest(role, digest))
100    }
101}
102
103impl std::fmt::Display for CanonicalIdentityRole {
104    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        formatter.write_str(self.as_str())
106    }
107}
108
109/// Renders the role-qualified digest, the form used by diagnostics and `inspect`.
110///
111/// This is a readable projection, not the equality contract: comparisons must
112/// use the whole [`CanonicalIdentity`], which also separates schema and
113/// algorithm.
114impl std::fmt::Display for CanonicalIdentity {
115    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        write!(formatter, "{}:{:032x}", self.role.as_str(), self.digest)
117    }
118}