Skip to content

Commit

Permalink
Use struct and impl for package family name
Browse files Browse the repository at this point in the history
  • Loading branch information
russellbanks committed Jul 29, 2024
1 parent 5eaad75 commit b7d6323
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 26 deletions.
9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "package-family-name"
version = "1.2.1"
version = "2.0.0"
edition = "2021"
license = "MIT OR Apache-2.0"
categories = ["no-std"]
Expand All @@ -10,5 +10,10 @@ readme = "README.md"
repository = "https://github.com/russellbanks/package-family-name"

[dependencies]
fast32 = "1"
fast32 = { version = "1", default-features = false }
heapless = { version = "0.8", default-features = false }
serde = { version = "1", default-features = false, optional = true }
sha2 = { version = "0.10", default-features = false }

[features]
serde = ["dep:serde"]
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Package Family Name

[![Test Status](https://github.com/russellbanks/package-family-name/workflows/Tests/badge.svg?event=push)](https://github.com/russellbanks/package-family-name/actions)
[![Test Status](https://github.com/russellbanks/package-family-name/workflows/Tests/badge.svg)](https://github.com/russellbanks/package-family-name/actions)

A Rust library for calculating MSIX Package Family Name values.

Expand All @@ -19,14 +19,11 @@ Add this to your `Cargo.toml`:

```toml
[dependencies]
package-family-name = "1.0"
package-family-name = "2"
```

This library currently only has one function, `get_package_family_name`, that takes in an identity name and an identity
publisher:

```rust
let package_family_name = get_package_family_name("AppName", "Publisher Software"); // AppName_zj75k085cmj1a
let package_family_name = PackageFamilyName::new("AppName", "Publisher Software"); // AppName_zj75k085cmj1a
```

## How a package family name is calculated
Expand All @@ -52,7 +49,8 @@ manifest.

## Acknowledgements

@marcinotorowski has produced a step by step explanation of how to calculate the hash part of the package family name.
[@marcinotorowski](https://github.com/marcinotorowski) has produced a step by step explanation of how to calculate the
hash part of the package family name.
This post can be found
[here](https://marcinotorowski.com/2021/12/19/calculating-hash-part-of-msix-package-family-name).

Expand Down
70 changes: 53 additions & 17 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,75 @@

extern crate alloc;

use alloc::format;
use alloc::borrow::ToOwned;
use alloc::string::String;
use core::fmt;
use core::str::FromStr;

use fast32::base32::CROCKFORD_LOWER;
use sha2::{Digest, Sha256};

pub fn get_package_family_name(identity_name: &str, identity_publisher: &str) -> String {
format!("{identity_name}_{}", get_publisher_hash(identity_publisher))
use crate::publisher_id::PublisherId;

mod publisher_id;

#[cfg(feature = "serde")]
mod serde;

#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct PackageFamilyName {
identity_name: String,
publisher_id: PublisherId,
}

impl fmt::Display for PackageFamilyName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}_{}", self.identity_name, self.publisher_id)
}
}

pub fn get_publisher_hash(identity_publisher: &str) -> String {
let publisher_sha_256 = identity_publisher
.encode_utf16()
.flat_map(u16::to_le_bytes)
.fold(Sha256::new(), |buf, byte| buf.chain_update([byte]))
.finalize();
impl PackageFamilyName {
pub fn new(identity_name: &str, identity_publisher: &str) -> Self {
PackageFamilyName {
identity_name: identity_name.to_owned(),
publisher_id: Self::get_id(identity_publisher),
}
}

pub fn get_id(identity_publisher: &str) -> PublisherId {
let publisher_sha_256 = identity_publisher
.encode_utf16()
.flat_map(u16::to_le_bytes)
.fold(Sha256::new(), |buf, byte| buf.chain_update([byte]))
.finalize();

CROCKFORD_LOWER.encode(&publisher_sha_256[..8])
let crockford_encoded = CROCKFORD_LOWER.encode(&publisher_sha_256[..8]);

PublisherId(heapless::String::from_str(&crockford_encoded).unwrap())
}
}

#[cfg(test)]
mod tests {
use crate::{get_package_family_name, get_publisher_hash};
use alloc::string::ToString;

use crate::PackageFamilyName;

#[test]
fn test_package_family_name() {
assert_eq!(
get_package_family_name("AppName", "Publisher Software"),
"AppName_zj75k085cmj1a"
);
let package_family_name = PackageFamilyName::new("AppName", "Publisher Software");
assert_eq!(package_family_name.to_string(), "AppName_zj75k085cmj1a");
}
#[test]
fn test_publisher_id() {
let publisher_id = PackageFamilyName::get_id("Publisher Software");
assert_eq!(publisher_id.to_string(), "zj75k085cmj1a");
}

#[test]
fn test_publisher_hash() {
assert_eq!(get_publisher_hash("Publisher Software"), "zj75k085cmj1a");
fn test_different_publishers() {
let publisher_id1 = PackageFamilyName::get_id("Publisher Software");
let publisher_id2 = PackageFamilyName::get_id("Another Publisher");
assert_ne!(publisher_id1.to_string(), publisher_id2.to_string());
}
}
49 changes: 49 additions & 0 deletions src/publisher_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use core::fmt;
use core::fmt::Display;
use core::str::FromStr;

const PUBLISHER_ID_LENGTH: usize = 13;

/// A 13-character long publisher ID
///
/// [Publisher Id](https://learn.microsoft.com/windows/apps/desktop/modernize/package-identity-overview#publisher-id)
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct PublisherId(pub heapless::String<PUBLISHER_ID_LENGTH>);

impl Display for PublisherId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}

impl FromStr for PublisherId {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() != PUBLISHER_ID_LENGTH {
return Err("expected publisher id length of 13");
}
Ok(PublisherId(heapless::String::from_str(s).unwrap()))
}
}

#[cfg(test)]
mod tests {
use crate::publisher_id::PublisherId;
use core::str::FromStr;

#[test]
fn test_publisher_id_from_str() {
assert!(PublisherId::from_str("zj75k085cmj1a").is_ok());
}

#[test]
fn test_publisher_id_too_short() {
assert!(PublisherId::from_str(&"1".repeat(3)).is_err());
}

#[test]
fn test_publisher_id_too_long() {
assert!(PublisherId::from_str(&"1".repeat(20)).is_err());
}
}
54 changes: 54 additions & 0 deletions src/serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use crate::publisher_id::PublisherId;
use crate::PackageFamilyName;
use alloc::borrow::ToOwned;
use alloc::string::ToString;
use core::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

impl<'identity> Serialize for PackageFamilyName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

impl<'de> Deserialize<'de> for PackageFamilyName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let deserialized_name = <&str>::deserialize(deserializer)?;
let (identity_name, identity_id) =
deserialized_name
.split_once('_')
.ok_or(serde::de::Error::custom(
"Invalid format for PackageFamilyName",
))?;
let publisher_id = PublisherId::from_str(identity_id).map_err(serde::de::Error::custom)?;
Ok(PackageFamilyName {
identity_name: identity_name.to_owned(),
publisher_id,
})
}
}

impl Serialize for PublisherId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}

impl<'de> Deserialize<'de> for PublisherId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let deserialized_id = <&str>::deserialize(deserializer)?;
PublisherId::from_str(deserialized_id).map_err(serde::de::Error::custom)
}
}

0 comments on commit b7d6323

Please sign in to comment.