1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! `PkgInfo` XML files.

use {
    crate::{distribution::Bundle, PkgResult},
    serde::{Deserialize, Serialize},
    std::io::Read,
};

/// Provides information about the package to install.
///
/// This includes authentication requirements, behavior after installation, etc.
/// See the fields for more descriptions.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct PackageInfo {
    /// Authentication requirements for the package install.
    ///
    /// Values include `none` and `root`.
    pub auth: String,

    #[serde(rename = "deleteObsoleteLanguages")]
    pub delete_obsolete_languages: Option<bool>,

    /// Whether symlinks found at install time should be resolved instead of being replaced by a
    /// real file or directory.
    #[serde(rename = "followSymLinks")]
    pub follow_symlinks: Option<bool>,

    /// Format version of the package.
    ///
    /// Value is likely `2`.
    pub format_version: u8,

    /// Identifies the tool that assembled this package.
    pub generator_version: Option<String>,

    /// Uniform type identifier that defines the package.
    ///
    /// Should ideally be unique to this package.
    pub identifier: String,

    /// Default location where the payload hierarchy should be installed.
    pub install_location: Option<String>,

    /// Defines minimum OS version on which the package can be installed.
    #[serde(rename = "minimumSystemVersion")]
    pub minimum_system_version: Option<bool>,

    /// Defines if permissions of existing directories should be updated with ones from the payload.
    pub overwrite_permissions: Option<bool>,

    /// Action to perform after install.
    ///
    /// Potential values can include `logout`, `restart`, and `shutdown`.
    pub postinstall_action: Option<String>,

    /// Preserve extended attributes on files.
    pub preserve_xattr: Option<bool>,

    /// Unknown.
    ///
    /// Probably has something to do with whether the installation tree can be relocated
    /// without issue.
    pub relocatable: Option<bool>,

    /// Whether items in the package should be compressed after installation.
    #[serde(rename = "useHFSPlusCompression")]
    pub use_hfs_plus_compression: Option<bool>,

    /// Version of the package.
    ///
    /// This is the version of the package itself, not the version of the application
    /// being installed.
    pub version: String,

    // End of attributes. Beginning of elements.
    #[serde(default)]
    pub atomic_update_bundle: Vec<BundleRef>,

    /// Versioning information about bundles within the payload.
    #[serde(default)]
    pub bundle: Vec<Bundle>,

    #[serde(default)]
    pub bundle_version: Vec<BundleRef>,

    /// Files to not obsolete during install.
    #[serde(default)]
    pub dont_obsolete: Vec<File>,

    /// Installs to process at next startup.
    #[serde(default)]
    pub install_at_startup: Vec<File>,

    /// Files to be patched.
    #[serde(default)]
    pub patch: Vec<File>,

    /// Provides information on the content being installed.
    pub payload: Option<Payload>,

    #[serde(default)]
    pub relocate: Vec<BundleRef>,

    /// Scripts to run before and after install.
    #[serde(default)]
    pub scripts: Scripts,

    #[serde(default)]
    pub strict_identifiers: Vec<BundleRef>,

    #[serde(default)]
    pub update_bundle: Vec<BundleRef>,

    #[serde(default)]
    pub upgrade_bundle: Vec<BundleRef>,
}

impl Default for PackageInfo {
    fn default() -> Self {
        Self {
            auth: "none".into(),
            delete_obsolete_languages: None,
            follow_symlinks: None,
            format_version: 2,
            generator_version: Some("rust-apple-flat-package".to_string()),
            identifier: "".to_string(),
            install_location: None,
            minimum_system_version: None,
            overwrite_permissions: None,
            postinstall_action: None,
            preserve_xattr: None,
            relocatable: None,
            use_hfs_plus_compression: None,
            version: "0".to_string(),
            atomic_update_bundle: vec![],
            bundle: vec![],
            bundle_version: vec![],
            dont_obsolete: vec![],
            install_at_startup: vec![],
            patch: vec![],
            payload: None,
            relocate: vec![],
            scripts: Default::default(),
            strict_identifiers: vec![],
            update_bundle: vec![],
            upgrade_bundle: vec![],
        }
    }
}

impl PackageInfo {
    /// Parse Distribution XML from a reader.
    pub fn from_reader(reader: impl Read) -> PkgResult<Self> {
        let mut de = serde_xml_rs::Deserializer::new_from_reader(reader);

        Ok(Self::deserialize(&mut de)?)
    }

    /// Parse Distribution XML from a string.
    pub fn from_xml(s: &str) -> PkgResult<Self> {
        let mut de = serde_xml_rs::Deserializer::new_from_reader(s.as_bytes())
            .non_contiguous_seq_elements(true);

        Ok(Self::deserialize(&mut de)?)
    }
}

/// File record.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct File {
    /// File path.
    pub path: String,

    /// Required SHA-1 of file.
    pub required_sha1: Option<String>,

    /// SHA-1 of file.
    pub sha1: Option<String>,
}

#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct Payload {
    #[serde(rename = "numberOfFiles")]
    pub number_of_files: u64,
    #[serde(rename = "installKBytes")]
    pub install_kbytes: u64,
}

#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct BundleRef {
    pub id: Option<String>,
}

/// Wrapper type to represent <scripts>.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct Scripts {
    #[serde(rename = "$value")]
    pub scripts: Vec<Script>,
}

/// An entry in <scripts>.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub enum Script {
    #[serde(rename = "preinstall")]
    PreInstall(PreInstall),

    #[serde(rename = "postinstall")]
    PostInstall(PostInstall),
}

/// A script to run before install.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct PreInstall {
    /// Name of script to run.
    pub file: String,

    /// ID of bundle element to run before.
    pub component_id: Option<String>,
}

/// A script to run after install.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct PostInstall {
    /// Name of script to run.
    pub file: String,

    /// ID of bundle element to run after.
    pub component_id: Option<String>,
}

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

    #[test]
    fn scripts_decode() {
        const INPUT: &str = r#"
            <?xml version="1.0" encoding="utf-8"?>
            <pkg-info overwrite-permissions="true" relocatable="false" identifier="my-app" postinstall-action="none" version="1" format-version="2" generator-version="InstallCmds-807 (21D62)" install-location="/usr/bin/my-app" auth="root">
                <payload numberOfFiles="123" installKBytes="123"/>
                <bundle-version/>
                <upgrade-bundle/>
                <update-bundle/>
                <atomic-update-bundle/>
                <strict-identifier/>
                <relocate/>
                <scripts>
                    <preinstall file="./preinstall"/>
                    <postinstall file="./postinstall"/>
                </scripts>
            </pkg-info>
        "#;

        let info = PackageInfo::from_xml(INPUT.trim()).unwrap();

        assert_eq!(
            info.scripts.scripts,
            vec![
                Script::PreInstall(PreInstall {
                    file: "./preinstall".into(),
                    component_id: None,
                }),
                Script::PostInstall(PostInstall {
                    file: "./postinstall".into(),
                    component_id: None,
                })
            ]
        );
    }
}