msix 0.4.0

Library for creating and signing msix packages.
Documentation
use anyhow::Result;
use serde::ser::{SerializeTuple, Serializer};
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename = "Package")]
pub struct AppxManifest {
    #[serde(rename(serialize = "xmlns"))]
    #[serde(default = "default_namespace")]
    ns: String,
    #[serde(rename(serialize = "xmlns:uap"))]
    #[serde(default = "default_uap_namespace")]
    ns_uap: String,
    #[serde(rename(serialize = "xmlns:rescap"))]
    #[serde(default = "default_rescap_namespace")]
    ns_rescap: String,
    #[serde(rename(serialize = "Identity"))]
    pub identity: Identity,
    #[serde(rename(serialize = "Properties"))]
    pub properties: Properties,
    #[serde(rename(serialize = "Resources"))]
    pub resources: Resources,
    #[serde(rename(serialize = "Dependencies"))]
    pub dependencies: Dependencies,
    #[serde(rename(serialize = "Capabilities"))]
    #[serde(serialize_with = "serialize_element")]
    pub capabilities: Vec<Capability>,
    #[serde(rename(serialize = "Applications"))]
    pub applications: Applications,
}

impl Default for AppxManifest {
    fn default() -> Self {
        Self {
            ns: default_namespace(),
            ns_uap: default_uap_namespace(),
            ns_rescap: default_rescap_namespace(),
            identity: Default::default(),
            properties: Default::default(),
            resources: Default::default(),
            dependencies: Default::default(),
            capabilities: Default::default(),
            applications: Default::default(),
        }
    }
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Applications {
    #[serde(rename(serialize = "Application"))]
    pub application: Vec<Application>,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Resources {
    #[serde(rename(serialize = "Resource"))]
    pub resource: Vec<Resource>,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Dependencies {
    #[serde(rename(serialize = "TargetDeviceFamily"))]
    pub target_device_family: Vec<TargetDeviceFamily>,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Identity {
    #[serde(rename(serialize = "Name"))]
    pub name: Option<String>,
    #[serde(rename(serialize = "Version"))]
    pub version: Option<String>,
    #[serde(rename(serialize = "Publisher"))]
    pub publisher: Option<String>,
    #[serde(rename(serialize = "ProcessorArchitecture"))]
    pub processor_architecture: Option<String>,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Properties {
    #[serde(rename(serialize = "DisplayName"))]
    #[serde(serialize_with = "serialize_element")]
    pub display_name: Option<String>,
    #[serde(rename(serialize = "PublisherDisplayName"))]
    #[serde(serialize_with = "serialize_element")]
    pub publisher_display_name: Option<String>,
    #[serde(rename(serialize = "Logo"))]
    #[serde(serialize_with = "serialize_element")]
    pub logo: Option<String>,
    #[serde(rename(serialize = "Description"))]
    #[serde(serialize_with = "serialize_element")]
    pub description: Option<String>,
}

fn serialize_element<S>(value: &impl Serialize, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    let mut tuple = serializer.serialize_tuple(1)?;
    tuple.serialize_element(value)?;
    tuple.end()
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Resource {
    #[serde(rename(serialize = "Language"))]
    pub language: String,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TargetDeviceFamily {
    #[serde(rename(serialize = "Name"))]
    pub name: String,
    #[serde(rename(serialize = "MinVersion"))]
    pub min_version: String,
    #[serde(rename(serialize = "MaxVersionTested"))]
    pub max_version: String,
}

impl Default for TargetDeviceFamily {
    fn default() -> Self {
        Self {
            name: "Windows.Desktop".into(),
            min_version: "10.0.0.0".into(),
            max_version: "10.0.20348.0".into(),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum Capability {
    #[serde(rename(deserialize = "capability"))]
    #[serde(rename(serialize = "Capability"))]
    Capability {
        #[serde(rename(serialize = "Name"))]
        name: String,
    },
    #[serde(rename(deserialize = "restricted"))]
    #[serde(rename(serialize = "rescap:Capability"))]
    Restricted {
        #[serde(rename(serialize = "Name"))]
        name: String,
    },
    #[serde(rename(deserialize = "device"))]
    #[serde(rename(serialize = "DeviceCapability"))]
    Device {
        #[serde(rename(serialize = "Name"))]
        name: String,
    },
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Application {
    #[serde(rename(serialize = "Id"))]
    pub id: Option<String>,
    #[serde(rename(serialize = "Executable"))]
    pub executable: Option<String>,
    #[serde(rename(serialize = "EntryPoint"))]
    pub entry_point: Option<String>,
    #[serde(rename(serialize = "uap:VisualElements"))]
    pub visual_elements: VisualElements,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct VisualElements {
    #[serde(rename(serialize = "BackgroundColor"))]
    pub background_color: Option<String>,
    #[serde(rename(serialize = "DisplayName"))]
    pub display_name: Option<String>,
    #[serde(rename(serialize = "Description"))]
    pub description: Option<String>,
    #[serde(rename(serialize = "Square150x150Logo"))]
    pub logo_150x150: Option<String>,
    #[serde(rename(serialize = "Square44x44Logo"))]
    pub logo_44x44: Option<String>,
    #[serde(rename(serialize = "uap:DefaultTile"))]
    pub default_tile: Option<DefaultTile>,
    #[serde(rename(serialize = "uap:SplashScreen"))]
    pub splash_screen: Option<SplashScreen>,
    #[serde(rename(serialize = "uap:LockScreen"))]
    pub lock_screen: Option<LockScreen>,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct DefaultTile {
    #[serde(rename(serialize = "ShortName"))]
    pub short_name: Option<String>,
    #[serde(rename(serialize = "Square71x71Logo"))]
    pub logo_71x71: Option<String>,
    #[serde(rename(serialize = "Square310x310Logo"))]
    pub logo_310x310: Option<String>,
    #[serde(rename(serialize = "Wide310x150Logo"))]
    pub logo_310x150: Option<String>,
    #[serde(rename(serialize = "uap:ShowNameOnTiles"))]
    pub show_names_on_tiles: ShowNameOnTiles,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct ShowNameOnTiles {
    #[serde(rename(serialize = "uap:ShowOn"))]
    pub show_on: Vec<ShowOn>,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct ShowOn {
    #[serde(rename(serialize = "Tile"))]
    pub tile: String,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct SplashScreen {
    #[serde(rename(serialize = "Image"))]
    pub image: String,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct LockScreen {
    #[serde(rename(serialize = "BadgeLogo"))]
    pub badge_logo: String,
    #[serde(rename(serialize = "Notification"))]
    pub notification: String,
}

fn default_namespace() -> String {
    "http://schemas.microsoft.com/appx/manifest/foundation/windows10".to_string()
}

fn default_uap_namespace() -> String {
    "http://schemas.microsoft.com/appx/manifest/uap/windows10".to_string()
}

fn default_rescap_namespace() -> String {
    "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
        .to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_properties() {
        let props = Properties {
            display_name: Some("".into()),
            publisher_display_name: Some("".into()),
            logo: Some("".into()),
            description: Some("".into()),
        };
        let xml = quick_xml::se::to_string(&props).unwrap();
        assert_eq!(xml, "<Properties><DisplayName></DisplayName><PublisherDisplayName></PublisherDisplayName><Logo></Logo><Description></Description></Properties>");
    }

    #[test]
    fn test_manifest() {
        let manifest = AppxManifest {
            identity: Identity {
                name: Some("com.flutter.fluttertodoapp".into()),
                version: Some("1.0.0.0".into()),
                publisher: Some(
                    "CN=Msix Testing, O=Msix Testing Corporation, S=Some-State, C=US".into(),
                ),
                processor_architecture: Some("x64".into()),
            },
            properties: Properties {
                display_name: Some("fluttertodoapp".into()),
                publisher_display_name: Some("com.flutter.fluttertodoapp".into()),
                logo: Some("Images\\StoreLogo.png".into()),
                description: Some("A new Flutter project.".into()),
            },
            resources: Resources {
                resource: vec![Resource {
                    language: "en".into(),
                }],
            },
            dependencies: Dependencies {
                target_device_family: vec![Default::default()],
            },
            capabilities: vec![
                Capability::Capability {
                    name: "internetClient".into(),
                },
                Capability::Restricted {
                    name: "runFullTrust".into(),
                },
                Capability::Device {
                    name: "location".into(),
                },
            ],
            applications: Applications {
                application: vec![Application {
                    id: Some("fluttertodoapp".into()),
                    executable: Some("todoapp.exe".into()),
                    entry_point: Some("Windows.FullTrustApplication".into()),
                    visual_elements: VisualElements {
                        background_color: Some("transparent".into()),
                        display_name: Some("fluttertodoapp".into()),
                        description: Some("A new flutter project.".into()),
                        logo_44x44: Some("Images\\Square44x44Logo.png".into()),
                        logo_150x150: Some("Images\\Square150x150Logo.png".into()),
                        default_tile: Some(DefaultTile {
                            short_name: Some("fluttertodoapp".into()),
                            logo_71x71: Some("Images\\SmallTile.png".into()),
                            logo_310x310: Some("Images\\LargeTile.png".into()),
                            logo_310x150: Some("Images\\Wide310x150Logo.png".into()),
                            show_names_on_tiles: ShowNameOnTiles {
                                show_on: vec![
                                    ShowOn {
                                        tile: "square150x150Logo".into(),
                                    },
                                    ShowOn {
                                        tile: "square310x310Logo".into(),
                                    },
                                    ShowOn {
                                        tile: "wide310x150Logo".into(),
                                    },
                                ],
                            },
                        }),
                        splash_screen: Some(SplashScreen {
                            image: "Images\\SplashScreen.png".into(),
                        }),
                        lock_screen: Some(LockScreen {
                            badge_logo: "Images\\BadgeLogo.png".into(),
                            notification: "badge".into(),
                        }),
                    },
                }],
            },
            ..Default::default()
        };
        let xml = quick_xml::se::to_string(&manifest).unwrap();
        println!("{}", xml);
    }
}