Skip to main content

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_26gmypax28ghe`. 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 = "3"
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#![no_std]
61
62extern crate alloc;
63
64mod crockford;
65mod publisher_id;
66
67use alloc::{borrow::ToOwned, boxed::Box};
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/windows/apps/desktop/modernize/package-identity-overview#package-family-name
93#[derive(Clone, Debug, Default, Eq)]
94pub struct PackageFamilyName {
95    package_name: Box<str>,
96    publisher_id: PublisherId,
97}
98
99impl PackageFamilyName {
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(package_name: &str, publisher: &str) -> Self {
119        Self {
120            package_name: package_name.into(),
121            publisher_id: PublisherId::new(publisher),
122        }
123    }
124
125    /// Returns the package name as a string slice.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// # use package_family_name::PackageFamilyName;
131    /// let package_family_name = PackageFamilyName::new(
132    ///     "Microsoft.PowerShell",
133    ///     "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
134    /// );
135    ///
136    /// assert_eq!(package_family_name.package_name(), "Microsoft.PowerShell");
137    /// ```
138    #[must_use]
139    #[inline]
140    pub fn package_name(&self) -> &str {
141        &self.package_name
142    }
143
144    /// Returns a reference to the [Publisher Id].
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// # use package_family_name::PackageFamilyName;
150    /// let package_family_name = PackageFamilyName::new(
151    ///     "Microsoft.PowerShell",
152    ///     "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
153    /// );
154    ///
155    /// assert_eq!(package_family_name.publisher_id().as_str(), "8wekyb3d8bbwe");
156    /// ```
157    ///
158    /// [Publisher Id]: PublisherId
159    #[must_use]
160    #[inline]
161    pub const fn publisher_id(&self) -> &PublisherId {
162        &self.publisher_id
163    }
164}
165
166impl fmt::Display for PackageFamilyName {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "{}_{}", self.package_name, self.publisher_id)
169    }
170}
171
172impl PartialEq for PackageFamilyName {
173    /// Tests for `self` and `other` values to be equal, and is used by `==`.
174    ///
175    /// Package Family Name is compared case-insensitively.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// # use package_family_name::PackageFamilyName;
181    /// let pfn_1 = PackageFamilyName::new("PowerShell", "CN=, O=, L=, S=, C=");
182    /// let pfn_2 = PackageFamilyName::new("powershell", "CN=, O=, L=, S=, C=");
183    ///
184    /// assert_eq!(pfn_1, pfn_2);
185    /// ```
186    fn eq(&self, other: &Self) -> bool {
187        self.package_name()
188            .eq_ignore_ascii_case(other.package_name())
189            && self.publisher_id() == other.publisher_id()
190    }
191}
192
193impl PartialOrd for PackageFamilyName {
194    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
195        Some(self.cmp(other))
196    }
197}
198
199impl Ord for PackageFamilyName {
200    fn cmp(&self, other: &Self) -> Ordering {
201        self.package_name()
202            .as_bytes()
203            .iter()
204            .map(u8::to_ascii_lowercase)
205            .cmp(
206                other
207                    .package_name()
208                    .as_bytes()
209                    .iter()
210                    .map(u8::to_ascii_lowercase),
211            )
212            .then_with(|| self.publisher_id().cmp(other.publisher_id()))
213    }
214}
215
216impl Hash for PackageFamilyName {
217    fn hash<H: Hasher>(&self, state: &mut H) {
218        for byte in self.package_name().as_bytes() {
219            state.write_u8(byte.to_ascii_lowercase());
220        }
221        state.write_u8(b'_');
222        self.publisher_id().hash(state);
223    }
224}
225
226#[derive(Error, Debug, Eq, PartialEq)]
227pub enum PackageFamilyNameError {
228    #[error(
229        "Package Family Name must have an underscore (`_`) between the package name and Publisher Id"
230    )]
231    NoUnderscore,
232    #[error(transparent)]
233    PublisherId(#[from] PublisherIdError),
234}
235
236impl FromStr for PackageFamilyName {
237    type Err = PackageFamilyNameError;
238
239    fn from_str(s: &str) -> Result<Self, Self::Err> {
240        let (package_name, publisher_id) = s.split_once('_').ok_or(Self::Err::NoUnderscore)?;
241
242        Ok(Self {
243            package_name: package_name.to_owned().into(),
244            publisher_id: publisher_id.parse()?,
245        })
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use alloc::string::ToString;
252    use core::cmp::Ordering;
253
254    use super::PackageFamilyName;
255
256    #[test]
257    fn microsoft_windows_photos() {
258        let package_family_name = PackageFamilyName::new(
259            "Microsoft.Windows.Photos",
260            "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US",
261        );
262
263        assert_eq!(
264            package_family_name.to_string(),
265            "Microsoft.Windows.Photos_8wekyb3d8bbwe"
266        );
267    }
268
269    #[test]
270    fn hydraulic_conveyor_15() {
271        let package_family_name = PackageFamilyName::new(
272            "Conveyor",
273            "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",
274        );
275
276        assert_eq!(package_family_name.to_string(), "Conveyor_fg3qp2cw01ypp");
277    }
278
279    #[test]
280    fn hydraulic_conveyor_16() {
281        let package_family_name = PackageFamilyName::new(
282            "Conveyor",
283            "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",
284        );
285
286        assert_eq!(package_family_name.to_string(), "Conveyor_r94jb655n6kcp");
287    }
288
289    #[test]
290    fn equality() {
291        let powershell_pfn_1 = "Microsoft.PowerShell_8wekyb3d8bbwe"
292            .parse::<PackageFamilyName>()
293            .unwrap();
294        let powershell_pfn_2 = "microsoft.powerShell_8WEKYB3D8BBWE"
295            .parse::<PackageFamilyName>()
296            .unwrap();
297
298        assert_eq!(powershell_pfn_1, powershell_pfn_1);
299        assert_eq!(powershell_pfn_1, powershell_pfn_2);
300        assert_ne!(
301            powershell_pfn_1,
302            "Conveyor_fg3qp2cw01ypp"
303                .parse::<PackageFamilyName>()
304                .unwrap()
305        );
306    }
307
308    #[test]
309    fn comparison() {
310        let powershell_pfn_1 = "Microsoft.PowerShell_8wekyb3d8bbwe"
311            .parse::<PackageFamilyName>()
312            .unwrap();
313        let powershell_pfn_2 = "microsoft.powerShell_8WEKYB3D8BBWE"
314            .parse::<PackageFamilyName>()
315            .unwrap();
316
317        assert_eq!(powershell_pfn_1.cmp(&powershell_pfn_1), Ordering::Equal);
318        assert_eq!(powershell_pfn_1.cmp(&powershell_pfn_2), Ordering::Equal);
319
320        let conveyor_pfn = "Conveyor_fg3qp2cw01ypp"
321            .parse::<PackageFamilyName>()
322            .unwrap();
323        assert_eq!(powershell_pfn_1.cmp(&conveyor_pfn), Ordering::Greater);
324        assert_eq!(conveyor_pfn.cmp(&powershell_pfn_1), Ordering::Less);
325    }
326
327    #[test]
328    fn hash() {
329        use core::hash::BuildHasher;
330
331        use rustc_hash::FxBuildHasher;
332
333        // If two keys are equal, their hashes must also be equal
334        // https://doc.rust-lang.org/std/hash/trait.Hash.html#hash-and-eq
335
336        let package_family_name_1 = "Microsoft.PowerShell_8wekyb3d8bbwe"
337            .parse::<PackageFamilyName>()
338            .unwrap();
339        let package_family_name_2 = "microsoft.powerShell_8WEKYB3D8BBWE"
340            .parse::<PackageFamilyName>()
341            .unwrap();
342        assert_eq!(package_family_name_1, package_family_name_2);
343
344        assert_eq!(
345            FxBuildHasher.hash_one(package_family_name_1),
346            FxBuildHasher.hash_one(package_family_name_2)
347        );
348    }
349
350    #[test]
351    fn size() {
352        assert_eq!(size_of::<PackageFamilyName>(), 32);
353    }
354
355    #[test]
356    fn alignment() {
357        assert_eq!(align_of::<PackageFamilyName>(), 8);
358    }
359}