package_family_name/
lib.rs

1/*!
2Package Family Name is a Rust crate for calculating MSIX Package Family Name values.
3
4Every MSIX application has a package family name value, which looks a bit like
5`AppName_zj75k085cmj1a`. This value can easily be found by running `Get-AppxPackage <name>` in
6PowerShell for an installed MSIX package and scrolling to `PackageFullName`.
7
8However, we can work out a package family name value without needing to install the package at all.
9That's where this library comes into play.
10
11## Usage
12
13Add this to your `Cargo.toml`:
14
15```toml
16[dependencies]
17package-family-name = "2"
18```
19
20```
21# use package_family_name::PackageFamilyName;
22let package_family_name = PackageFamilyName::new(
23    "Microsoft.PowerShell",
24    "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
25);
26
27assert_eq!(package_family_name.to_string(), "Microsoft.PowerShell_8wekyb3d8bbwe");
28```
29
30## How a package family name is calculated
31
32In short, a package family name is made up of two parts:
33
34- Identity name (`Microsoft.PowerShell`)
35- Identity publisher (`CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US`)
36
37These steps are then taken:
38
391. UTF-16 encode the identity publisher
402. Calculate a SHA256 hash of the encoded publisher
413. Take the first 8 bytes of the hash
424. Encode the result with [Douglas Crockford Base32](http://www.crockford.com/base32.html)
435. Join the identity name and the encoded value with an underscore (`Microsoft.PowerShell_8wekyb3d8bbwe`)
44
45### Why would I need to calculate a package family name?
46
47Whilst this is a niche library, there are use cases. For example, when submitting an MSIX package to
48[winget-pkgs](https://github.com/microsoft/winget-pkgs), a package family name value is a required
49as part of the manifest.
50
51## Acknowledgements
52
53[@marcinotorowski](https://github.com/marcinotorowski) has produced a step by step explanation of
54how to calculate the hash part of the package family name.
55This post can be found
56[here](https://marcinotorowski.com/2021/12/19/calculating-hash-part-of-msix-package-family-name).
57*/
58
59#![doc(html_root_url = "https://docs.rs/package-family-name")]
60#![forbid(unsafe_code)]
61#![no_std]
62
63extern crate alloc;
64
65mod publisher_id;
66
67use alloc::borrow::{Cow, ToOwned};
68use core::{
69    cmp::Ordering,
70    fmt,
71    hash::{Hash, Hasher},
72    str::FromStr,
73};
74
75pub use publisher_id::{PublisherId, PublisherIdError};
76use thiserror::Error;
77
78#[cfg(feature = "serde")]
79mod serde;
80
81/// A [Package Family Name] is an opaque string derived from only two parts of a package identity -
82/// name and publisher.
83///
84/// `<Name>_<PublisherId>`
85///
86/// For example, the Package Family Name of the Windows Photos app is
87/// `Microsoft.Windows.Photos_8wekyb3d8bbwe`, where `Microsoft.Windows.Photos` is the name and
88/// `8wekyb3d8bbwe` is the publisher ID for Microsoft.
89///
90/// Package Family Name is often referred to as a 'version-less Package Full Name'.
91///
92/// [Package Family Name]: https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/package-identity-overview#package-family-name
93#[derive(Clone, Debug, Default, Eq)]
94pub struct PackageFamilyName<'ident> {
95    package_name: Cow<'ident, str>,
96    publisher_id: PublisherId,
97}
98
99impl<'ident> PackageFamilyName<'ident> {
100    /// Creates a new Package Family Name from a package name and an identity publisher.
101    ///
102    /// This is equivalent to the Windows function [`PackageNameAndPublisherIdFromFamilyName`].
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// # use package_family_name::PackageFamilyName;
108    /// let package_family_name = PackageFamilyName::new(
109    ///     "Microsoft.PowerShell",
110    ///     "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
111    /// );
112    ///
113    /// assert_eq!(package_family_name.to_string(), "Microsoft.PowerShell_8wekyb3d8bbwe");
114    /// ```
115    ///
116    /// [`PackageNameAndPublisherIdFromFamilyName`]: https://learn.microsoft.com/en-us/windows/win32/api/appmodel/nf-appmodel-packagenameandpublisheridfromfamilyname
117    #[must_use]
118    pub fn new<T, S>(package_name: T, identity_publisher: S) -> Self
119    where
120        T: Into<Cow<'ident, str>>,
121        S: AsRef<str>,
122    {
123        Self {
124            package_name: package_name.into(),
125            publisher_id: PublisherId::new(identity_publisher),
126        }
127    }
128
129    /// Returns the package name as a string slice.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// # use package_family_name::PackageFamilyName;
135    /// let package_family_name = PackageFamilyName::new(
136    ///     "Microsoft.PowerShell",
137    ///     "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
138    /// );
139    ///
140    /// assert_eq!(package_family_name.package_name(), "Microsoft.PowerShell");
141    /// ```
142    #[must_use]
143    #[inline]
144    pub fn package_name(&self) -> &str {
145        &self.package_name
146    }
147
148    /// Returns a reference to the [Publisher Id].
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// # use package_family_name::PackageFamilyName;
154    /// let package_family_name = PackageFamilyName::new(
155    ///     "Microsoft.PowerShell",
156    ///     "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
157    /// );
158    ///
159    /// assert_eq!(package_family_name.publisher_id().as_str(), "8wekyb3d8bbwe");
160    /// ```
161    ///
162    /// [Publisher Id]: PublisherId
163    #[must_use]
164    #[inline]
165    pub const fn publisher_id(&self) -> &PublisherId {
166        &self.publisher_id
167    }
168}
169
170impl fmt::Display for PackageFamilyName<'_> {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        write!(f, "{}_{}", self.package_name, self.publisher_id)
173    }
174}
175
176impl PartialEq for PackageFamilyName<'_> {
177    /// Tests for `self` and `other` values to be equal, and is used by `==`.
178    ///
179    /// Package Family Name is compared case-insensitively.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// # use package_family_name::PackageFamilyName;
185    /// let pfn_1 = PackageFamilyName::new("PowerShell", "CN=, O=, L=, S=, C=");
186    /// let pfn_2 = PackageFamilyName::new("powershell", "CN=, O=, L=, S=, C=");
187    ///
188    /// assert_eq!(pfn_1, pfn_2);
189    /// ```
190    fn eq(&self, other: &Self) -> bool {
191        self.package_name()
192            .eq_ignore_ascii_case(other.package_name())
193            && self.publisher_id().eq(other.publisher_id())
194    }
195}
196
197impl PartialOrd for PackageFamilyName<'_> {
198    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
199        Some(self.cmp(other))
200    }
201}
202
203impl Ord for PackageFamilyName<'_> {
204    fn cmp(&self, other: &Self) -> Ordering {
205        self.package_name()
206            .as_bytes()
207            .iter()
208            .map(u8::to_ascii_lowercase)
209            .cmp(
210                other
211                    .package_name()
212                    .as_bytes()
213                    .iter()
214                    .map(u8::to_ascii_lowercase),
215            )
216            .then_with(|| self.publisher_id().cmp(other.publisher_id()))
217    }
218}
219
220impl Hash for PackageFamilyName<'_> {
221    fn hash<H: Hasher>(&self, state: &mut H) {
222        for byte in self.package_name().as_bytes() {
223            state.write_u8(byte.to_ascii_lowercase());
224        }
225        state.write_u8(b'_');
226        self.publisher_id().hash(state);
227    }
228}
229
230#[derive(Error, Debug, Eq, PartialEq)]
231pub enum PackageFamilyNameError {
232    #[error(
233        "Package Family Name must have an underscore (`_`) between the package name and Publisher Id"
234    )]
235    NoUnderscore,
236    #[error(transparent)]
237    PublisherId(#[from] PublisherIdError),
238}
239
240impl FromStr for PackageFamilyName<'_> {
241    type Err = PackageFamilyNameError;
242
243    fn from_str(s: &str) -> Result<Self, Self::Err> {
244        let (package_name, publisher_id) = s.split_once('_').ok_or(Self::Err::NoUnderscore)?;
245
246        Ok(Self {
247            package_name: package_name.to_owned().into(),
248            publisher_id: publisher_id.parse()?,
249        })
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use alloc::string::ToString;
256    use core::{
257        cmp::Ordering,
258        hash::{BuildHasher, Hash, Hasher},
259    };
260
261    use super::PackageFamilyName;
262
263    #[test]
264    fn microsoft_windows_photos() {
265        let package_family_name = PackageFamilyName::new(
266            "Microsoft.Windows.Photos",
267            "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US",
268        );
269
270        assert_eq!(
271            package_family_name.to_string(),
272            "Microsoft.Windows.Photos_8wekyb3d8bbwe"
273        );
274    }
275
276    #[test]
277    fn hydraulic_conveyor_15() {
278        let package_family_name = PackageFamilyName::new(
279            "Conveyor",
280            "CN=Hydraulic Software AG, O=Hydraulic Software AG, L=Zürich, S=Zürich, C=CH, SERIALNUMBER=CHE-312.597.948, OID.1.3.6.1.4.1.311.60.2.1.2=Zürich, OID.1.3.6.1.4.1.311.60.2.1.3=CH, OID.2.5.4.15=Private Organization",
281        );
282
283        assert_eq!(package_family_name.to_string(), "Conveyor_fg3qp2cw01ypp");
284    }
285
286    #[test]
287    fn hydraulic_conveyor_16() {
288        let package_family_name = PackageFamilyName::new(
289            "Conveyor",
290            "CN=Hydraulic Software AG, O=Hydraulic Software AG, L=Zürich, S=Zürich, C=CH, SERIALNUMBER=CHE-312.597.948, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Zürich, OID.1.3.6.1.4.1.311.60.2.1.3=CH",
291        );
292
293        assert_eq!(package_family_name.to_string(), "Conveyor_r94jb655n6kcp");
294    }
295
296    #[test]
297    fn equality() {
298        let powershell_pfn_1 = "Microsoft.PowerShell_8wekyb3d8bbwe"
299            .parse::<PackageFamilyName>()
300            .unwrap();
301        let powershell_pfn_2 = "microsoft.powerShell_8WEKYB3D8BBWE"
302            .parse::<PackageFamilyName>()
303            .unwrap();
304
305        assert_eq!(powershell_pfn_1, powershell_pfn_1);
306        assert_eq!(powershell_pfn_1, powershell_pfn_2);
307        assert_ne!(
308            powershell_pfn_1,
309            "Conveyor_fg3qp2cw01ypp"
310                .parse::<PackageFamilyName>()
311                .unwrap()
312        );
313    }
314
315    #[test]
316    fn comparison() {
317        let powershell_pfn_1 = "Microsoft.PowerShell_8wekyb3d8bbwe"
318            .parse::<PackageFamilyName>()
319            .unwrap();
320        let powershell_pfn_2 = "microsoft.powerShell_8WEKYB3D8BBWE"
321            .parse::<PackageFamilyName>()
322            .unwrap();
323
324        assert_eq!(powershell_pfn_1.cmp(&powershell_pfn_1), Ordering::Equal);
325        assert_eq!(powershell_pfn_1.cmp(&powershell_pfn_2), Ordering::Equal);
326
327        let conveyor_pfn = "Conveyor_fg3qp2cw01ypp"
328            .parse::<PackageFamilyName>()
329            .unwrap();
330        assert_eq!(powershell_pfn_1.cmp(&conveyor_pfn), Ordering::Greater);
331        assert_eq!(conveyor_pfn.cmp(&powershell_pfn_1), Ordering::Less);
332    }
333
334    #[test]
335    fn hash() {
336        // If two keys are equal, their hashes must also be equal
337        // https://doc.rust-lang.org/std/hash/trait.Hash.html#hash-and-eq
338
339        let package_family_name_1 = "Microsoft.PowerShell_8wekyb3d8bbwe"
340            .parse::<PackageFamilyName>()
341            .unwrap();
342        let package_family_name_2 = "microsoft.powerShell_8WEKYB3D8BBWE"
343            .parse::<PackageFamilyName>()
344            .unwrap();
345        assert_eq!(package_family_name_1, package_family_name_2);
346
347        let state = foldhash::fast::RandomState::default();
348        let mut package_family_name_1_hasher = state.build_hasher();
349        package_family_name_1.hash(&mut package_family_name_1_hasher);
350
351        let mut package_family_name_2_hasher = state.build_hasher();
352        package_family_name_2.hash(&mut package_family_name_2_hasher);
353
354        assert_eq!(
355            package_family_name_1_hasher.finish(),
356            package_family_name_2_hasher.finish()
357        );
358    }
359}