apple_flat_package/reader.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Reading support for Apple flat package (`.pkg`) installers.
6
7use {
8 crate::{
9 component_package::ComponentPackageReader, distribution::Distribution, Error, PkgResult,
10 },
11 apple_xar::reader::XarReader,
12 std::{
13 fmt::Debug,
14 io::{Cursor, Read, Seek},
15 },
16};
17
18/// The type of a flat package.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum PkgFlavor {
21 /// A *component* installer.
22 ///
23 /// This consists of a single component.
24 Component,
25
26 /// A *product* installer.
27 ///
28 /// This consists of multiple components, described by a `Distribution` file.
29 Product,
30}
31
32/// Read-only interface to a single flat package XAR archive.
33pub struct PkgReader<R: Read + Seek + Sized + Debug> {
34 xar: XarReader<R>,
35 flavor: PkgFlavor,
36}
37
38impl<R: Read + Seek + Sized + Debug> PkgReader<R> {
39 /// Construct an instance from a reader.
40 ///
41 /// The reader will read the contents of a XAR archive. This is likely
42 /// a `.pkg` file.
43 pub fn new(reader: R) -> PkgResult<Self> {
44 let xar = XarReader::new(reader)?;
45
46 let flavor = if xar.find_file("Distribution")?.is_some() {
47 PkgFlavor::Product
48 } else {
49 PkgFlavor::Component
50 };
51
52 Ok(Self { xar, flavor })
53 }
54
55 /// Return the inner reader, consuming self.
56 pub fn into_inner(self) -> XarReader<R> {
57 self.xar
58 }
59
60 /// Obtain the flavor of the flat package.
61 pub fn flavor(&self) -> PkgFlavor {
62 self.flavor
63 }
64
65 /// Obtain the parsed `Distribution` XML file describing the installer.
66 ///
67 /// Not all flat packages have a `Distribution` file, so this may resolve to
68 /// `None`.
69 pub fn distribution(&mut self) -> PkgResult<Option<Distribution>> {
70 if let Some(xml_data) = self.xar.get_file_data_from_path("Distribution")? {
71 Ok(Some(Distribution::from_reader(Cursor::new(xml_data))?))
72 } else {
73 Ok(None)
74 }
75 }
76
77 /// Attempt to resolve a component given a path prefix.
78 ///
79 /// If a component is found under a given path, `Some` is returned. Otherwise
80 /// `None` is returned.
81 ///
82 /// Pass in `""` to resolve the root component.
83 ///
84 /// A *found* component is defined by the presence of 1 or more well-known files
85 /// in components (`Bom`, `PackageInfo`, `Payload`, etc).
86 ///
87 pub fn resolve_component(
88 &mut self,
89 path_prefix: &str,
90 ) -> PkgResult<Option<ComponentPackageReader>> {
91 let prefix = if path_prefix.is_empty() {
92 "".to_string()
93 } else {
94 format!("{path_prefix}/")
95 };
96
97 let mut bom_data = None;
98 let mut package_info_data = None;
99 let mut payload_data = None;
100 let mut scripts_data = None;
101
102 for (filename, file) in self
103 .xar
104 .files()?
105 .into_iter()
106 .filter(|(filename, _)| filename.starts_with(&prefix))
107 {
108 let mut data = Vec::<u8>::with_capacity(file.size.unwrap_or(0) as _);
109 self.xar
110 .write_file_data_decoded_from_file(&file, &mut data)?;
111
112 let filename = filename.strip_prefix(&prefix).expect("prefix should match");
113
114 match filename {
115 "Bom" => {
116 bom_data = Some(data);
117 }
118 "PackageInfo" => {
119 package_info_data = Some(data);
120 }
121 "Payload" => {
122 payload_data = Some(data);
123 }
124 "Scripts" => {
125 scripts_data = Some(data);
126 }
127 _ => {}
128 }
129 }
130
131 if bom_data.is_some()
132 || package_info_data.is_some()
133 || payload_data.is_some()
134 || scripts_data.is_some()
135 {
136 Ok(Some(ComponentPackageReader::from_file_data(
137 bom_data,
138 package_info_data,
139 payload_data,
140 scripts_data,
141 )?))
142 } else {
143 Ok(None)
144 }
145 }
146
147 /// Obtain the *root* component in this installer.
148 ///
149 /// The *root component* is defined as a a collection of `Bom`, `PackageInfo`,
150 /// `Payload`, and `Scripts` files at the root directory of the XAR archive
151 /// this instance was constructed from.
152 ///
153 /// This will likely resolve to `None` for *product packages` and `Some`
154 /// for *component packages*.
155 pub fn root_component(&mut self) -> PkgResult<Option<ComponentPackageReader>> {
156 self.resolve_component("")
157 }
158
159 /// Obtain *component package* instances in this flat package.
160 ///
161 /// This looks for `.pkg` directories in the root directory of the XAR archive
162 /// and resolves a [ComponentPackageReader] for each. If there are no `.pkg`
163 /// directories, this will return an empty vec.
164 ///
165 /// Generally, this function will return something for *product packages*
166 /// whereas [root_component()] will return something for *component packages*.
167 pub fn component_packages(&mut self) -> PkgResult<Vec<ComponentPackageReader>> {
168 // TODO obtain instances from Distribution XML instead of scanning filenames.
169 let components = self
170 .xar
171 .files()?
172 .into_iter()
173 .filter_map(|(filename, _)| {
174 if filename.ends_with(".pkg") && !filename.contains('/') {
175 Some(filename)
176 } else {
177 None
178 }
179 })
180 .collect::<Vec<_>>();
181
182 let mut res = vec![];
183
184 for component in components {
185 res.push(
186 self.resolve_component(&component)?
187 .ok_or(Error::ComponentResolution)?,
188 );
189 }
190
191 Ok(res)
192 }
193}