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}