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}