apple_flat_package/
reader.rs

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
// 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/.

//! Reading support for Apple flat package (`.pkg`) installers.

use {
    crate::{
        component_package::ComponentPackageReader, distribution::Distribution, Error, PkgResult,
    },
    apple_xar::reader::XarReader,
    std::{
        fmt::Debug,
        io::{Cursor, Read, Seek},
    },
};

/// The type of a flat package.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PkgFlavor {
    /// A *component* installer.
    ///
    /// This consists of a single component.
    Component,

    /// A *product* installer.
    ///
    /// This consists of multiple components, described by a `Distribution` file.
    Product,
}

/// Read-only interface to a single flat package XAR archive.
pub struct PkgReader<R: Read + Seek + Sized + Debug> {
    xar: XarReader<R>,
    flavor: PkgFlavor,
}

impl<R: Read + Seek + Sized + Debug> PkgReader<R> {
    /// Construct an instance from a reader.
    ///
    /// The reader will read the contents of a XAR archive. This is likely
    /// a `.pkg` file.
    pub fn new(reader: R) -> PkgResult<Self> {
        let xar = XarReader::new(reader)?;

        let flavor = if xar.find_file("Distribution")?.is_some() {
            PkgFlavor::Product
        } else {
            PkgFlavor::Component
        };

        Ok(Self { xar, flavor })
    }

    /// Return the inner reader, consuming self.
    pub fn into_inner(self) -> XarReader<R> {
        self.xar
    }

    /// Obtain the flavor of the flat package.
    pub fn flavor(&self) -> PkgFlavor {
        self.flavor
    }

    /// Obtain the parsed `Distribution` XML file describing the installer.
    ///
    /// Not all flat packages have a `Distribution` file, so this may resolve to
    /// `None`.
    pub fn distribution(&mut self) -> PkgResult<Option<Distribution>> {
        if let Some(xml_data) = self.xar.get_file_data_from_path("Distribution")? {
            Ok(Some(Distribution::from_reader(Cursor::new(xml_data))?))
        } else {
            Ok(None)
        }
    }

    /// Attempt to resolve a component given a path prefix.
    ///
    /// If a component is found under a given path, `Some` is returned. Otherwise
    /// `None` is returned.
    ///
    /// Pass in `""` to resolve the root component.
    ///
    /// A *found* component is defined by the presence of 1 or more well-known files
    /// in components (`Bom`, `PackageInfo`, `Payload`, etc).
    ///
    pub fn resolve_component(
        &mut self,
        path_prefix: &str,
    ) -> PkgResult<Option<ComponentPackageReader>> {
        let prefix = if path_prefix.is_empty() {
            "".to_string()
        } else {
            format!("{path_prefix}/")
        };

        let mut bom_data = None;
        let mut package_info_data = None;
        let mut payload_data = None;
        let mut scripts_data = None;

        for (filename, file) in self
            .xar
            .files()?
            .into_iter()
            .filter(|(filename, _)| filename.starts_with(&prefix))
        {
            let mut data = Vec::<u8>::with_capacity(file.size.unwrap_or(0) as _);
            self.xar
                .write_file_data_decoded_from_file(&file, &mut data)?;

            let filename = filename.strip_prefix(&prefix).expect("prefix should match");

            match filename {
                "Bom" => {
                    bom_data = Some(data);
                }
                "PackageInfo" => {
                    package_info_data = Some(data);
                }
                "Payload" => {
                    payload_data = Some(data);
                }
                "Scripts" => {
                    scripts_data = Some(data);
                }
                _ => {}
            }
        }

        if bom_data.is_some()
            || package_info_data.is_some()
            || payload_data.is_some()
            || scripts_data.is_some()
        {
            Ok(Some(ComponentPackageReader::from_file_data(
                bom_data,
                package_info_data,
                payload_data,
                scripts_data,
            )?))
        } else {
            Ok(None)
        }
    }

    /// Obtain the *root* component in this installer.
    ///
    /// The *root component* is defined as a a collection of `Bom`, `PackageInfo`,
    /// `Payload`, and `Scripts` files at the root directory of the XAR archive
    /// this instance was constructed from.
    ///
    /// This will likely resolve to `None` for *product packages` and `Some`
    /// for *component packages*.
    pub fn root_component(&mut self) -> PkgResult<Option<ComponentPackageReader>> {
        self.resolve_component("")
    }

    /// Obtain *component package* instances in this flat package.
    ///
    /// This looks for `.pkg` directories in the root directory of the XAR archive
    /// and resolves a [ComponentPackageReader] for each. If there are no `.pkg`
    /// directories, this will return an empty vec.
    ///
    /// Generally, this function will return something for *product packages*
    /// whereas [root_component()] will return something for *component packages*.
    pub fn component_packages(&mut self) -> PkgResult<Vec<ComponentPackageReader>> {
        // TODO obtain instances from Distribution XML instead of scanning filenames.
        let components = self
            .xar
            .files()?
            .into_iter()
            .filter_map(|(filename, _)| {
                if filename.ends_with(".pkg") && !filename.contains('/') {
                    Some(filename)
                } else {
                    None
                }
            })
            .collect::<Vec<_>>();

        let mut res = vec![];

        for component in components {
            res.push(
                self.resolve_component(&component)?
                    .ok_or(Error::ComponentResolution)?,
            );
        }

        Ok(res)
    }
}