1use std::collections::BTreeSet;
2use std::fs::{self, DirBuilder};
3use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
4use std::path::{Path, PathBuf};
5
6use super::agent::Agent;
7use super::files::{Payload, bounded, entry};
8#[cfg(test)]
9use super::fixtures::Hooks;
10use super::plan::{Plan, destination, json, text};
11use super::publication::{Stage, stale_stages, sync_path};
12use super::types::{Error, Manifest, Report};
13
14pub fn install(binary: &Path, config: &Path, directory: &Path) -> Result<Report, Error> {
19 install_with(
20 binary,
21 config,
22 directory,
23 #[cfg(test)]
24 &mut Hooks::default(),
25 )
26}
27
28pub(super) fn install_with(
29 binary: &Path,
30 config: &Path,
31 directory: &Path,
32 #[cfg(test)] hooks: &mut Hooks,
33) -> Result<Report, Error> {
34 install_from(
35 || Ok(binary),
36 config,
37 directory,
38 None,
39 #[cfg(test)]
40 hooks,
41 )
42}
43
44pub(super) fn install_current(
45 config: &Path,
46 directory: &Path,
47 agent: Option<&Agent>,
48 source: impl FnOnce() -> Result<PathBuf, Error>,
49) -> Result<Report, Error> {
50 install_from(
51 source,
52 config,
53 directory,
54 agent,
55 #[cfg(test)]
56 &mut Hooks::default(),
57 )
58}
59
60fn install_from<B: AsRef<Path>>(
61 source: impl FnOnce() -> Result<B, Error>,
62 config: &Path,
63 directory: &Path,
64 agent: Option<&Agent>,
65 #[cfg(test)] hooks: &mut Hooks,
66) -> Result<Report, Error> {
67 let directory = destination(directory)?;
68 let stages = stale_stages(&directory)?;
69 let outcome = source().and_then(|binary| {
70 install_to(
71 binary.as_ref(),
72 config,
73 &directory,
74 agent,
75 #[cfg(test)]
76 hooks,
77 )
78 });
79 match outcome {
80 Ok(mut report) => {
81 report.stale_stages = stages;
82 Ok(report)
83 }
84 Err(error) => Err(error.with_stale_stages(stages)),
85 }
86}
87
88fn install_to(
89 binary: &Path,
90 config: &Path,
91 directory: &Path,
92 agent: Option<&Agent>,
93 #[cfg(test)] hooks: &mut Hooks,
94) -> Result<Report, Error> {
95 let mut plan = match agent {
96 Some(agent) => Plan::with_agent(binary, config, directory, Some(agent))?,
97 None => Plan::new(binary, config, directory)?,
98 };
99 match fs::symlink_metadata(directory) {
100 Ok(metadata) => {
101 if !metadata.is_dir() || metadata.file_type().is_symlink() {
102 return Err(Error::new(
103 "bundle_destination_invalid",
104 "bundle destination is not a real directory",
105 ));
106 }
107 verify(directory, &plan.manifest)?;
108 durable(
109 directory,
110 &plan.manifest,
111 #[cfg(test)]
112 hooks,
113 )?;
114 return report(directory, plan.manifest.files.len(), true);
115 }
116 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
117 Err(error) => return Err(Error::new("bundle_destination_invalid", &error)),
118 }
119 let mut stage = Stage::create(
120 directory,
121 #[cfg(test)]
122 hooks,
123 )?;
124 let mut completed = Vec::new();
125 let result = write(
126 stage.path(),
127 &mut plan,
128 &mut completed,
129 #[cfg(test)]
130 hooks,
131 )
132 .and_then(|()| verify(stage.path(), &plan.manifest))
133 .and_then(|()| {
134 stage.publish(
135 directory,
136 #[cfg(test)]
137 hooks,
138 )
139 });
140 if let Err(error) = result {
141 let mut error = error.with_partial(&completed, stage.published);
142 if let Err(cleanup) = stage.cleanup(
143 #[cfg(test)]
144 hooks,
145 ) {
146 error = error.with_cleanup(&cleanup);
147 }
148 return Err(error);
149 }
150 report(directory, plan.manifest.files.len(), false)
151}
152
153fn report(directory: &Path, files: usize, already_installed: bool) -> Result<Report, Error> {
154 Ok(Report {
155 directory: text(directory)?.to_owned(),
156 files,
157 already_installed,
158 stale_stages: Vec::new(),
159 })
160}
161
162fn directories(manifest: &Manifest) -> BTreeSet<String> {
163 let mut directories = BTreeSet::new();
164 for name in manifest.files.keys() {
165 let mut path = Path::new(name).parent();
166 while let Some(parent) = path {
167 if let Some(name) = parent.to_str().filter(|name| !name.is_empty()) {
168 directories.insert(name.to_owned());
169 }
170 path = parent.parent();
171 }
172 }
173 directories
174}
175
176fn write(
177 directory: &Path,
178 plan: &mut Plan,
179 completed: &mut Vec<String>,
180 #[cfg(test)] hooks: &mut Hooks,
181) -> Result<(), Error> {
182 let directories = directories(&plan.manifest);
183 for name in &directories {
184 DirBuilder::new()
185 .mode(0o700)
186 .create(directory.join(name))
187 .map_err(|error| Error::new("bundle_write_failed", &error))?;
188 }
189 for (name, payload) in &mut plan.payloads {
190 let entry =
191 plan.manifest.files.get(name).ok_or_else(|| {
192 Error::new("bundle_plan_invalid", "payload has no manifest entry")
193 })?;
194 let path = directory.join(name);
195 #[cfg(test)]
196 hooks.payload(completed.len() + 1, &path)?;
197 payload.write(
198 &path,
199 entry,
200 #[cfg(test)]
201 hooks,
202 )?;
203 completed.push(name.clone());
204 }
205 let mut manifest = Payload::Bytes(json(&plan.manifest)?);
206 let manifest_entry = manifest.entry(0o600)?;
207 manifest.write(
208 &directory.join("bundle.json"),
209 &manifest_entry,
210 #[cfg(test)]
211 hooks,
212 )?;
213 sync_directories(
214 directory,
215 &plan.manifest,
216 #[cfg(test)]
217 hooks,
218 )
219}
220
221fn durable(
222 directory: &Path,
223 manifest: &Manifest,
224 #[cfg(test)] hooks: &mut Hooks,
225) -> Result<(), Error> {
226 for name in manifest
227 .files
228 .keys()
229 .map(String::as_str)
230 .chain(std::iter::once("bundle.json"))
231 {
232 sync_path(
233 &directory.join(name),
234 #[cfg(test)]
235 hooks,
236 )?;
237 }
238 sync_directories(
239 directory,
240 manifest,
241 #[cfg(test)]
242 hooks,
243 )?;
244 sync_path(
245 directory
246 .parent()
247 .ok_or_else(|| Error::new("bundle_path_invalid", "bundle parent missing"))?,
248 #[cfg(test)]
249 hooks,
250 )
251}
252
253fn sync_directories(
254 directory: &Path,
255 manifest: &Manifest,
256 #[cfg(test)] hooks: &mut Hooks,
257) -> Result<(), Error> {
258 for name in directories(manifest).iter().rev() {
259 sync_path(
260 &directory.join(name),
261 #[cfg(test)]
262 hooks,
263 )?;
264 }
265 sync_path(
266 directory,
267 #[cfg(test)]
268 hooks,
269 )
270}
271
272fn verify(directory: &Path, expected: &Manifest) -> Result<(), Error> {
273 let bytes = bounded(&directory.join("bundle.json"), "bundle_manifest_invalid")?;
274 let actual: Manifest = serde_json::from_slice(&bytes)
275 .map_err(|error| Error::new("bundle_manifest_invalid", &error))?;
276 if actual != *expected {
277 return Err(Error::new(
278 "bundle_conflict",
279 "bundle manifest differs from the requested inputs",
280 ));
281 }
282 for (name, expected) in &actual.files {
283 if entry(&directory.join(name), "bundle_payload_invalid")? != *expected {
284 return Err(Error::new(
285 "bundle_payload_invalid",
286 &format!("payload does not match manifest: {name}"),
287 ));
288 }
289 }
290 let directories = directories(&actual);
291 let mut pending = vec![String::new()];
292 let mut seen = BTreeSet::new();
293 while let Some(relative) = pending.pop() {
294 let path = directory.join(&relative);
295 let metadata = fs::symlink_metadata(&path)
296 .map_err(|error| Error::new("bundle_inventory_invalid", &error))?;
297 if !metadata.is_dir() || metadata.permissions().mode() & 0o7777 != 0o700 {
298 return Err(Error::new(
299 "bundle_inventory_invalid",
300 "bundle directory type or permissions changed",
301 ));
302 }
303 for child in
304 fs::read_dir(path).map_err(|error| Error::new("bundle_inventory_invalid", &error))?
305 {
306 let child = child.map_err(|error| Error::new("bundle_inventory_invalid", &error))?;
307 let name = child
308 .file_name()
309 .into_string()
310 .map_err(|_| Error::new("bundle_inventory_invalid", "bundle entry is not UTF-8"))?;
311 let name = if relative.is_empty() {
312 name
313 } else {
314 format!("{relative}/{name}")
315 };
316 if directories.contains(&name) {
317 pending.push(name);
318 } else if actual.files.contains_key(&name) {
319 seen.insert(name);
320 } else if name == "bundle.json" {
321 if entry(&child.path(), "bundle_manifest_invalid")?.mode != 0o600 {
322 return Err(Error::new(
323 "bundle_manifest_invalid",
324 "manifest permissions changed",
325 ));
326 }
327 } else {
328 return Err(Error::new(
329 "bundle_inventory_invalid",
330 "unexpected bundle entry",
331 ));
332 }
333 }
334 }
335 if seen.len() != actual.files.len() {
336 return Err(Error::new(
337 "bundle_inventory_invalid",
338 "bundle inventory is incomplete",
339 ));
340 }
341 Ok(())
342}