]>
Commit | Line | Data |
---|---|---|
25cdd0e0 | 1 | use std::collections::HashMap; |
03f779c6 | 2 | use std::convert::TryFrom; |
c443f58b WB |
3 | use std::ffi::{CStr, CString, OsStr, OsString}; |
4 | use std::future::Future; | |
f14c96ea | 5 | use std::io::Write; |
c443f58b | 6 | use std::mem; |
f14c96ea | 7 | use std::os::unix::ffi::OsStrExt; |
c443f58b WB |
8 | use std::os::unix::io::{AsRawFd, FromRawFd}; |
9 | use std::path::{Path, PathBuf}; | |
10 | use std::pin::Pin; | |
f14c96ea | 11 | |
f7d4e4b5 | 12 | use anyhow::{bail, format_err, Error}; |
c443f58b WB |
13 | use nix::dir::Dir; |
14 | use nix::fcntl::OFlag; | |
15 | use nix::sys::stat::Mode; | |
f14c96ea | 16 | |
c443f58b WB |
17 | use pathpatterns::{MatchEntry, MatchList, MatchPattern, MatchType, PatternFlag}; |
18 | use proxmox::api::api; | |
19 | use proxmox::api::cli::{self, CliCommand, CliCommandMap, CliHelper, CommandLineInterface}; | |
20 | use proxmox::c_result; | |
21 | use proxmox::tools::fs::{create_path, CreateOptions}; | |
22 | use pxar::{EntryKind, Metadata}; | |
501f4fa2 | 23 | |
c443f58b | 24 | use crate::backup::catalog::{self, DirEntryAttribute}; |
f14c96ea | 25 | |
c443f58b | 26 | use crate::pxar::dir_stack::PxarDirStack; |
5444fa94 | 27 | use crate::pxar::Flags; |
c443f58b WB |
28 | use crate::pxar::fuse::{Accessor, FileEntry}; |
29 | use crate::pxar::metadata; | |
f14c96ea | 30 | |
c443f58b | 31 | type CatalogReader = crate::backup::CatalogReader<std::fs::File>; |
951cf17e | 32 | |
c443f58b WB |
33 | const MAX_SYMLINK_COUNT: usize = 40; |
34 | ||
35 | static mut SHELL: Option<usize> = None; | |
951cf17e | 36 | |
ecbaa38f DM |
37 | /// This list defines all the shell commands and their properties |
38 | /// using the api schema | |
55c3cb69 | 39 | pub fn catalog_shell_cli() -> CommandLineInterface { |
c443f58b WB |
40 | CommandLineInterface::Nested( |
41 | CliCommandMap::new() | |
42 | .insert("pwd", CliCommand::new(&API_METHOD_PWD_COMMAND)) | |
43 | .insert( | |
44 | "cd", | |
45 | CliCommand::new(&API_METHOD_CD_COMMAND) | |
46 | .arg_param(&["path"]) | |
47 | .completion_cb("path", complete_path), | |
48 | ) | |
49 | .insert( | |
50 | "ls", | |
51 | CliCommand::new(&API_METHOD_LS_COMMAND) | |
52 | .arg_param(&["path"]) | |
53 | .completion_cb("path", complete_path), | |
54 | ) | |
55 | .insert( | |
56 | "stat", | |
57 | CliCommand::new(&API_METHOD_STAT_COMMAND) | |
58 | .arg_param(&["path"]) | |
59 | .completion_cb("path", complete_path), | |
60 | ) | |
61 | .insert( | |
62 | "select", | |
63 | CliCommand::new(&API_METHOD_SELECT_COMMAND) | |
64 | .arg_param(&["path"]) | |
65 | .completion_cb("path", complete_path), | |
66 | ) | |
67 | .insert( | |
68 | "deselect", | |
69 | CliCommand::new(&API_METHOD_DESELECT_COMMAND) | |
70 | .arg_param(&["path"]) | |
71 | .completion_cb("path", complete_path), | |
72 | ) | |
73 | .insert( | |
74 | "clear-selected", | |
75 | CliCommand::new(&API_METHOD_CLEAR_SELECTED_COMMAND), | |
76 | ) | |
77 | .insert( | |
78 | "list-selected", | |
79 | CliCommand::new(&API_METHOD_LIST_SELECTED_COMMAND), | |
80 | ) | |
81 | .insert( | |
82 | "restore-selected", | |
83 | CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND) | |
84 | .arg_param(&["target"]) | |
85 | .completion_cb("target", crate::tools::complete_file_name), | |
86 | ) | |
87 | .insert( | |
88 | "restore", | |
89 | CliCommand::new(&API_METHOD_RESTORE_COMMAND) | |
90 | .arg_param(&["target"]) | |
91 | .completion_cb("target", crate::tools::complete_file_name), | |
92 | ) | |
93 | .insert( | |
94 | "find", | |
95 | CliCommand::new(&API_METHOD_FIND_COMMAND).arg_param(&["pattern"]), | |
96 | ) | |
97 | .insert_help(), | |
98 | ) | |
ecbaa38f DM |
99 | } |
100 | ||
c443f58b WB |
101 | fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> { |
102 | let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) }; | |
103 | match shell.complete_path(complete_me) { | |
104 | Ok(list) => list, | |
105 | Err(err) => { | |
106 | eprintln!("error during completion: {}", err); | |
107 | Vec::new() | |
951cf17e | 108 | } |
951cf17e CE |
109 | } |
110 | } | |
111 | ||
6934c6fe CE |
112 | #[api(input: { properties: {} })] |
113 | /// List the current working directory. | |
c443f58b WB |
114 | async fn pwd_command() -> Result<(), Error> { |
115 | Shell::with(move |shell| shell.pwd()).await | |
951cf17e CE |
116 | } |
117 | ||
6934c6fe CE |
118 | #[api( |
119 | input: { | |
120 | properties: { | |
121 | path: { | |
122 | type: String, | |
123 | optional: true, | |
124 | description: "target path." | |
125 | } | |
126 | } | |
951cf17e | 127 | } |
6934c6fe CE |
128 | )] |
129 | /// Change the current working directory to the new directory | |
c443f58b WB |
130 | async fn cd_command(path: Option<String>) -> Result<(), Error> { |
131 | let path = path.as_ref().map(Path::new); | |
132 | Shell::with(move |shell| shell.cd(path)).await | |
951cf17e CE |
133 | } |
134 | ||
6934c6fe CE |
135 | #[api( |
136 | input: { | |
137 | properties: { | |
138 | path: { | |
139 | type: String, | |
140 | optional: true, | |
141 | description: "target path." | |
142 | } | |
951cf17e CE |
143 | } |
144 | } | |
6934c6fe CE |
145 | )] |
146 | /// List the content of working directory or given path. | |
c443f58b WB |
147 | async fn ls_command(path: Option<String>) -> Result<(), Error> { |
148 | let path = path.as_ref().map(Path::new); | |
149 | Shell::with(move |shell| shell.ls(path)).await | |
6934c6fe CE |
150 | } |
151 | ||
152 | #[api( | |
153 | input: { | |
154 | properties: { | |
155 | path: { | |
156 | type: String, | |
157 | description: "target path." | |
158 | } | |
951cf17e | 159 | } |
951cf17e | 160 | } |
6934c6fe CE |
161 | )] |
162 | /// Read the metadata for a given directory entry. | |
163 | /// | |
c443f58b WB |
164 | /// This is expensive because the data has to be read from the pxar archive, which means reading |
165 | /// over the network. | |
166 | async fn stat_command(path: String) -> Result<(), Error> { | |
167 | Shell::with(move |shell| shell.stat(PathBuf::from(path))).await | |
951cf17e CE |
168 | } |
169 | ||
6934c6fe CE |
170 | #[api( |
171 | input: { | |
172 | properties: { | |
173 | path: { | |
174 | type: String, | |
175 | description: "target path." | |
176 | } | |
177 | } | |
178 | } | |
179 | )] | |
180 | /// Select an entry for restore. | |
181 | /// | |
182 | /// This will return an error if the entry is already present in the list or | |
183 | /// if an invalid path was provided. | |
c443f58b WB |
184 | async fn select_command(path: String) -> Result<(), Error> { |
185 | Shell::with(move |shell| shell.select(PathBuf::from(path))).await | |
f14c96ea CE |
186 | } |
187 | ||
6934c6fe CE |
188 | #[api( |
189 | input: { | |
190 | properties: { | |
191 | path: { | |
192 | type: String, | |
193 | description: "path to entry to remove from list." | |
194 | } | |
195 | } | |
f14c96ea | 196 | } |
6934c6fe CE |
197 | )] |
198 | /// Deselect an entry for restore. | |
199 | /// | |
200 | /// This will return an error if the entry was not found in the list of entries | |
201 | /// selected for restore. | |
c443f58b WB |
202 | async fn deselect_command(path: String) -> Result<(), Error> { |
203 | Shell::with(move |shell| shell.deselect(PathBuf::from(path))).await | |
6934c6fe | 204 | } |
f14c96ea | 205 | |
35ddf0b4 CE |
206 | #[api( input: { properties: { } })] |
207 | /// Clear the list of files selected for restore. | |
c443f58b WB |
208 | async fn clear_selected_command() -> Result<(), Error> { |
209 | Shell::with(move |shell| shell.deselect_all()).await | |
35ddf0b4 CE |
210 | } |
211 | ||
6934c6fe CE |
212 | #[api( |
213 | input: { | |
214 | properties: { | |
c443f58b WB |
215 | patterns: { |
216 | type: Boolean, | |
217 | description: "List match patterns instead of the matching files.", | |
218 | optional: true, | |
219 | default: false, | |
6934c6fe CE |
220 | } |
221 | } | |
f14c96ea | 222 | } |
6934c6fe | 223 | )] |
c443f58b WB |
224 | /// List entries currently selected for restore. |
225 | async fn list_selected_command(patterns: bool) -> Result<(), Error> { | |
226 | Shell::with(move |shell| shell.list_selected(patterns)).await | |
6934c6fe | 227 | } |
f14c96ea | 228 | |
8e464141 CE |
229 | #[api( |
230 | input: { | |
231 | properties: { | |
232 | pattern: { | |
c443f58b WB |
233 | type: String, |
234 | description: "Match pattern for matching files in the catalog." | |
235 | }, | |
236 | select: { | |
237 | type: bool, | |
8e464141 | 238 | optional: true, |
c443f58b WB |
239 | default: false, |
240 | description: "Add matching filenames to list for restore." | |
8e464141 CE |
241 | } |
242 | } | |
243 | } | |
244 | )] | |
c443f58b WB |
245 | /// Find entries in the catalog matching the given match pattern. |
246 | async fn find_command(pattern: String, select: bool) -> Result<(), Error> { | |
247 | Shell::with(move |shell| shell.find(pattern, select)).await | |
6934c6fe | 248 | } |
f14c96ea | 249 | |
6934c6fe CE |
250 | #[api( |
251 | input: { | |
252 | properties: { | |
253 | target: { | |
254 | type: String, | |
255 | description: "target path for restore on local filesystem." | |
6934c6fe CE |
256 | } |
257 | } | |
258 | } | |
259 | )] | |
c443f58b | 260 | /// Restore the selected entries to the given target path. |
6934c6fe | 261 | /// |
c443f58b WB |
262 | /// Target must not exist on the clients filesystem. |
263 | async fn restore_selected_command(target: String) -> Result<(), Error> { | |
264 | Shell::with(move |shell| shell.restore_selected(PathBuf::from(target))).await | |
6934c6fe CE |
265 | } |
266 | ||
25cdd0e0 CE |
267 | #[api( |
268 | input: { | |
269 | properties: { | |
c443f58b | 270 | target: { |
25cdd0e0 | 271 | type: String, |
c443f58b | 272 | description: "target path for restore on local filesystem." |
25cdd0e0 CE |
273 | }, |
274 | pattern: { | |
275 | type: String, | |
25cdd0e0 | 276 | optional: true, |
c443f58b | 277 | description: "match pattern to limit files for restore." |
25cdd0e0 CE |
278 | } |
279 | } | |
280 | } | |
281 | )] | |
c443f58b WB |
282 | /// Restore the sub-archive given by the current working directory to target. |
283 | /// | |
284 | /// By further providing a pattern, the restore can be limited to a narrower | |
285 | /// subset of this sub-archive. | |
286 | /// If pattern is not present or empty, the full archive is restored to target. | |
287 | async fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error> { | |
288 | Shell::with(move |shell| shell.restore(PathBuf::from(target), pattern)).await | |
289 | } | |
25cdd0e0 | 290 | |
26e78a2e WB |
291 | /// TODO: Should we use this to fix `step()`? Make path resolution behave more like described in |
292 | /// the path_resolution(7) man page. | |
c443f58b WB |
293 | /// |
294 | /// The `Path` type's component iterator does not tell us anything about trailing slashes or | |
295 | /// trailing `Component::CurDir` entries. Since we only support regular paths we'll roll our own | |
296 | /// here: | |
297 | enum PathComponent<'a> { | |
298 | Root, | |
299 | CurDir, | |
300 | ParentDir, | |
301 | Normal(&'a OsStr), | |
302 | TrailingSlash, | |
303 | } | |
304 | ||
305 | struct PathComponentIter<'a> { | |
306 | path: &'a [u8], | |
307 | state: u8, // 0=beginning, 1=ongoing, 2=trailing, 3=finished (fused) | |
308 | } | |
25cdd0e0 | 309 | |
c443f58b WB |
310 | impl std::iter::FusedIterator for PathComponentIter<'_> {} |
311 | ||
312 | impl<'a> Iterator for PathComponentIter<'a> { | |
313 | type Item = PathComponent<'a>; | |
314 | ||
315 | fn next(&mut self) -> Option<Self::Item> { | |
316 | if self.path.is_empty() { | |
317 | return None; | |
25cdd0e0 CE |
318 | } |
319 | ||
c443f58b WB |
320 | if self.state == 0 { |
321 | self.state = 1; | |
322 | if self.path[0] == b'/' { | |
323 | // absolute path | |
324 | self.path = &self.path[1..]; | |
325 | return Some(PathComponent::Root); | |
326 | } | |
327 | } | |
25cdd0e0 | 328 | |
c443f58b WB |
329 | // skip slashes |
330 | let had_slashes = self.path[0] == b'/'; | |
331 | while self.path.get(0).copied() == Some(b'/') { | |
332 | self.path = &self.path[1..]; | |
333 | } | |
334 | ||
335 | Some(match self.path { | |
336 | [] if had_slashes => PathComponent::TrailingSlash, | |
337 | [] => return None, | |
338 | [b'.'] | [b'.', b'/', ..] => { | |
339 | self.path = &self.path[1..]; | |
340 | PathComponent::CurDir | |
341 | } | |
342 | [b'.', b'.'] | [b'.', b'.', b'/', ..] => { | |
343 | self.path = &self.path[2..]; | |
344 | PathComponent::ParentDir | |
345 | } | |
346 | _ => { | |
347 | let end = self | |
348 | .path | |
349 | .iter() | |
350 | .position(|&b| b == b'/') | |
351 | .unwrap_or(self.path.len()); | |
352 | let (out, rest) = self.path.split_at(end); | |
353 | self.path = rest; | |
354 | PathComponent::Normal(OsStr::from_bytes(out)) | |
355 | } | |
356 | }) | |
357 | } | |
6934c6fe CE |
358 | } |
359 | ||
c443f58b WB |
360 | pub struct Shell { |
361 | /// Readline instance handling input and callbacks | |
362 | rl: rustyline::Editor<CliHelper>, | |
363 | ||
364 | /// Interactive prompt. | |
365 | prompt: String, | |
366 | ||
6934c6fe | 367 | /// Calalog reader instance to navigate |
c443f58b WB |
368 | catalog: CatalogReader, |
369 | ||
6934c6fe | 370 | /// List of selected paths for restore |
c443f58b WB |
371 | selected: HashMap<OsString, MatchEntry>, |
372 | ||
373 | /// pxar accessor instance for the current pxar archive | |
374 | accessor: Accessor, | |
375 | ||
376 | /// The current position in the archive. | |
377 | position: Vec<PathStackEntry>, | |
6934c6fe CE |
378 | } |
379 | ||
c443f58b WB |
380 | #[derive(Clone)] |
381 | struct PathStackEntry { | |
382 | /// This is always available. We mainly navigate through the catalog. | |
383 | catalog: catalog::DirEntry, | |
384 | ||
385 | /// Whenever we need something from the actual archive we fill this out. This is cached along | |
386 | /// the entire path. | |
387 | pxar: Option<FileEntry>, | |
388 | } | |
389 | ||
390 | impl PathStackEntry { | |
391 | fn new(dir_entry: catalog::DirEntry) -> Self { | |
392 | Self { | |
393 | pxar: None, | |
394 | catalog: dir_entry, | |
395 | } | |
396 | } | |
397 | } | |
398 | ||
399 | impl Shell { | |
400 | /// Create a new shell for the given catalog and pxar archive. | |
401 | pub async fn new( | |
402 | mut catalog: CatalogReader, | |
403 | archive_name: &str, | |
404 | archive: Accessor, | |
405 | ) -> Result<Self, Error> { | |
406 | let cli_helper = CliHelper::new(catalog_shell_cli()); | |
407 | let mut rl = rustyline::Editor::<CliHelper>::new(); | |
408 | rl.set_helper(Some(cli_helper)); | |
409 | ||
410 | let catalog_root = catalog.root()?; | |
411 | let archive_root = catalog | |
412 | .lookup(&catalog_root, archive_name.as_bytes())? | |
413 | .ok_or_else(|| format_err!("archive not found in catalog"))?; | |
414 | let position = vec![PathStackEntry::new(archive_root)]; | |
415 | ||
416 | let mut this = Self { | |
417 | rl, | |
418 | prompt: String::new(), | |
419 | catalog, | |
420 | selected: HashMap::new(), | |
421 | accessor: archive, | |
422 | position, | |
423 | }; | |
424 | this.update_prompt(); | |
425 | Ok(this) | |
426 | } | |
427 | ||
428 | async fn with<'a, Fut, R, F>(call: F) -> Result<R, Error> | |
6934c6fe | 429 | where |
c443f58b WB |
430 | F: FnOnce(&'a mut Shell) -> Fut, |
431 | Fut: Future<Output = Result<R, Error>>, | |
432 | F: 'a, | |
433 | Fut: 'a, | |
434 | R: 'static, | |
6934c6fe | 435 | { |
c443f58b WB |
436 | let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) }; |
437 | let result = call(&mut *shell).await; | |
438 | result | |
f14c96ea CE |
439 | } |
440 | ||
c443f58b WB |
441 | pub async fn shell(mut self) -> Result<(), Error> { |
442 | let this = &mut self; | |
443 | unsafe { | |
444 | SHELL = Some(this as *mut Shell as usize); | |
445 | } | |
446 | while let Ok(line) = this.rl.readline(&this.prompt) { | |
447 | let helper = this.rl.helper().unwrap(); | |
448 | let args = match cli::shellword_split(&line) { | |
449 | Ok(args) => args, | |
450 | Err(err) => { | |
451 | println!("Error: {}", err); | |
452 | continue; | |
453 | } | |
454 | }; | |
455 | ||
456 | let _ = | |
457 | cli::handle_command_future(helper.cmd_def(), "", args, cli::CliEnvironment::new()) | |
458 | .await; | |
459 | this.rl.add_history_entry(line); | |
460 | this.update_prompt(); | |
461 | } | |
462 | Ok(()) | |
463 | } | |
464 | ||
465 | fn update_prompt(&mut self) { | |
466 | self.prompt = "pxar:".to_string(); | |
467 | if self.position.len() <= 1 { | |
468 | self.prompt.push('/'); | |
469 | } else { | |
470 | for p in self.position.iter().skip(1) { | |
471 | if !p.catalog.name.starts_with(b"/") { | |
472 | self.prompt.push('/'); | |
473 | } | |
474 | match std::str::from_utf8(&p.catalog.name) { | |
475 | Ok(entry) => self.prompt.push_str(entry), | |
476 | Err(_) => self.prompt.push_str("<non-utf8-dir>"), | |
477 | } | |
f14c96ea CE |
478 | } |
479 | } | |
c443f58b WB |
480 | self.prompt.push_str(" > "); |
481 | } | |
482 | ||
483 | async fn pwd(&mut self) -> Result<(), Error> { | |
484 | let stack = Self::lookup( | |
485 | &self.position, | |
486 | &mut self.catalog, | |
487 | &self.accessor, | |
488 | None, | |
489 | &mut Some(0), | |
490 | ) | |
491 | .await?; | |
492 | let path = Self::format_path_stack(&stack); | |
493 | println!("{:?}", path); | |
494 | Ok(()) | |
f14c96ea CE |
495 | } |
496 | ||
c443f58b WB |
497 | fn new_path_stack(&self) -> Vec<PathStackEntry> { |
498 | self.position[..1].to_vec() | |
f14c96ea | 499 | } |
fee5528e | 500 | |
c443f58b WB |
501 | async fn resolve_symlink( |
502 | stack: &mut Vec<PathStackEntry>, | |
503 | catalog: &mut CatalogReader, | |
504 | accessor: &Accessor, | |
505 | follow_symlinks: &mut Option<usize>, | |
506 | ) -> Result<(), Error> { | |
507 | if let Some(ref mut symlink_count) = follow_symlinks { | |
508 | *symlink_count += 1; | |
509 | if *symlink_count > MAX_SYMLINK_COUNT { | |
510 | bail!("too many levels of symbolic links"); | |
511 | } | |
fee5528e | 512 | |
c443f58b WB |
513 | let file = Self::walk_pxar_archive(accessor, &mut stack[..]).await?; |
514 | ||
515 | let path = match file.entry().kind() { | |
516 | EntryKind::Symlink(symlink) => Path::new(symlink.as_os_str()), | |
517 | _ => bail!("symlink in the catalog was not a symlink in the archive"), | |
518 | }; | |
519 | ||
520 | let new_stack = | |
521 | Self::lookup(&stack, &mut *catalog, accessor, Some(path), follow_symlinks).await?; | |
522 | ||
523 | *stack = new_stack; | |
524 | ||
525 | Ok(()) | |
526 | } else { | |
527 | bail!("target is a symlink"); | |
fee5528e CE |
528 | } |
529 | } | |
530 | ||
c443f58b WB |
531 | /// Walk a path and add it to the path stack. |
532 | /// | |
533 | /// If the symlink count is used, symlinks will be followed, until we hit the cap and error | |
534 | /// out. | |
535 | async fn step( | |
536 | stack: &mut Vec<PathStackEntry>, | |
537 | catalog: &mut CatalogReader, | |
538 | accessor: &Accessor, | |
539 | component: std::path::Component<'_>, | |
540 | follow_symlinks: &mut Option<usize>, | |
541 | ) -> Result<(), Error> { | |
542 | use std::path::Component; | |
543 | match component { | |
544 | Component::Prefix(_) => bail!("invalid path component (prefix)"), | |
545 | Component::RootDir => stack.truncate(1), | |
546 | Component::CurDir => { | |
547 | if stack.last().unwrap().catalog.is_symlink() { | |
548 | Self::resolve_symlink(stack, catalog, accessor, follow_symlinks).await?; | |
549 | } | |
550 | } | |
551 | Component::ParentDir => drop(stack.pop()), | |
552 | Component::Normal(entry) => { | |
553 | if stack.last().unwrap().catalog.is_symlink() { | |
554 | Self::resolve_symlink(stack, catalog, accessor, follow_symlinks).await?; | |
555 | } | |
556 | match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? { | |
557 | Some(dir) => stack.push(PathStackEntry::new(dir)), | |
558 | None => bail!("no such file or directory: {:?}", entry), | |
559 | } | |
560 | } | |
561 | } | |
562 | ||
563 | Ok(()) | |
fee5528e CE |
564 | } |
565 | ||
c443f58b WB |
566 | fn step_nofollow( |
567 | stack: &mut Vec<PathStackEntry>, | |
568 | catalog: &mut CatalogReader, | |
569 | component: std::path::Component<'_>, | |
570 | ) -> Result<(), Error> { | |
571 | use std::path::Component; | |
572 | match component { | |
573 | Component::Prefix(_) => bail!("invalid path component (prefix)"), | |
574 | Component::RootDir => stack.truncate(1), | |
575 | Component::CurDir => { | |
576 | if stack.last().unwrap().catalog.is_symlink() { | |
577 | bail!("target is a symlink"); | |
578 | } | |
579 | } | |
580 | Component::ParentDir => drop(stack.pop()), | |
581 | Component::Normal(entry) => { | |
582 | if stack.last().unwrap().catalog.is_symlink() { | |
583 | bail!("target is a symlink"); | |
584 | } else { | |
585 | match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? { | |
586 | Some(dir) => stack.push(PathStackEntry::new(dir)), | |
587 | None => bail!("no such file or directory: {:?}", entry), | |
588 | } | |
589 | } | |
590 | } | |
591 | } | |
592 | Ok(()) | |
593 | } | |
594 | ||
595 | /// The pxar accessor is required to resolve symbolic links | |
596 | async fn walk_catalog( | |
597 | stack: &mut Vec<PathStackEntry>, | |
598 | catalog: &mut CatalogReader, | |
599 | accessor: &Accessor, | |
600 | path: &Path, | |
601 | follow_symlinks: &mut Option<usize>, | |
602 | ) -> Result<(), Error> { | |
603 | for c in path.components() { | |
604 | Self::step(stack, catalog, accessor, c, follow_symlinks).await?; | |
605 | } | |
606 | Ok(()) | |
607 | } | |
608 | ||
609 | /// Non-async version cannot follow symlinks. | |
610 | fn walk_catalog_nofollow( | |
611 | stack: &mut Vec<PathStackEntry>, | |
612 | catalog: &mut CatalogReader, | |
613 | path: &Path, | |
614 | ) -> Result<(), Error> { | |
615 | for c in path.components() { | |
616 | Self::step_nofollow(stack, catalog, c)?; | |
617 | } | |
618 | Ok(()) | |
619 | } | |
620 | ||
621 | /// This assumes that there are no more symlinks in the path stack. | |
622 | async fn walk_pxar_archive( | |
623 | accessor: &Accessor, | |
624 | mut stack: &mut [PathStackEntry], | |
625 | ) -> Result<FileEntry, Error> { | |
626 | if stack[0].pxar.is_none() { | |
627 | stack[0].pxar = Some(accessor.open_root().await?.lookup_self().await?); | |
628 | } | |
629 | ||
630 | // Now walk the directory stack: | |
631 | let mut at = 1; | |
632 | while at < stack.len() { | |
633 | if stack[at].pxar.is_some() { | |
634 | at += 1; | |
635 | continue; | |
636 | } | |
637 | ||
638 | let parent = stack[at - 1].pxar.as_ref().unwrap(); | |
639 | let dir = parent.enter_directory().await?; | |
640 | let name = Path::new(OsStr::from_bytes(&stack[at].catalog.name)); | |
641 | stack[at].pxar = Some( | |
642 | dir.lookup(name) | |
643 | .await? | |
644 | .ok_or_else(|| format_err!("no such entry in pxar file: {:?}", name))?, | |
645 | ); | |
646 | ||
647 | at += 1; | |
648 | } | |
649 | ||
650 | Ok(stack.last().unwrap().pxar.clone().unwrap()) | |
651 | } | |
652 | ||
653 | fn complete_path(&mut self, input: &str) -> Result<Vec<String>, Error> { | |
654 | let mut tmp_stack; | |
655 | let (parent, base, part) = match input.rfind('/') { | |
656 | Some(ind) => { | |
657 | let (base, part) = input.split_at(ind + 1); | |
658 | let path = PathBuf::from(base); | |
659 | if path.is_absolute() { | |
660 | tmp_stack = self.new_path_stack(); | |
661 | } else { | |
662 | tmp_stack = self.position.clone(); | |
663 | } | |
664 | Self::walk_catalog_nofollow(&mut tmp_stack, &mut self.catalog, &path)?; | |
665 | (&tmp_stack.last().unwrap().catalog, base, part) | |
666 | } | |
667 | None => (&self.position.last().unwrap().catalog, "", input), | |
668 | }; | |
669 | ||
670 | let entries = self.catalog.read_dir(parent)?; | |
671 | ||
672 | let mut out = Vec::new(); | |
673 | for entry in entries { | |
674 | let mut name = base.to_string(); | |
675 | if entry.name.starts_with(part.as_bytes()) { | |
676 | name.push_str(std::str::from_utf8(&entry.name)?); | |
677 | if entry.is_directory() { | |
678 | name.push('/'); | |
679 | } | |
680 | out.push(name); | |
681 | } | |
682 | } | |
683 | ||
684 | Ok(out) | |
685 | } | |
686 | ||
687 | // Break async recursion here: lookup -> walk_catalog -> step -> lookup | |
688 | fn lookup<'future, 's, 'c, 'a, 'p, 'y>( | |
689 | stack: &'s [PathStackEntry], | |
690 | catalog: &'c mut CatalogReader, | |
691 | accessor: &'a Accessor, | |
692 | path: Option<&'p Path>, | |
693 | follow_symlinks: &'y mut Option<usize>, | |
694 | ) -> Pin<Box<dyn Future<Output = Result<Vec<PathStackEntry>, Error>> + Send + 'future>> | |
695 | where | |
696 | 's: 'future, | |
697 | 'c: 'future, | |
698 | 'a: 'future, | |
699 | 'p: 'future, | |
700 | 'y: 'future, | |
701 | { | |
702 | Box::pin(async move { | |
703 | Ok(match path { | |
704 | None => stack.to_vec(), | |
705 | Some(path) => { | |
706 | let mut stack = if path.is_absolute() { | |
707 | stack[..1].to_vec() | |
708 | } else { | |
709 | stack.to_vec() | |
710 | }; | |
711 | Self::walk_catalog(&mut stack, catalog, accessor, path, follow_symlinks) | |
712 | .await?; | |
713 | stack | |
714 | } | |
715 | }) | |
716 | }) | |
717 | } | |
718 | ||
719 | async fn ls(&mut self, path: Option<&Path>) -> Result<(), Error> { | |
720 | let stack = Self::lookup( | |
721 | &self.position, | |
722 | &mut self.catalog, | |
723 | &self.accessor, | |
724 | path, | |
725 | &mut Some(0), | |
726 | ) | |
727 | .await?; | |
728 | ||
729 | let last = stack.last().unwrap(); | |
730 | if last.catalog.is_directory() { | |
731 | let items = self.catalog.read_dir(&stack.last().unwrap().catalog)?; | |
732 | let mut out = std::io::stdout(); | |
733 | // FIXME: columnize | |
734 | for item in items { | |
735 | out.write_all(&item.name)?; | |
736 | out.write_all(b"\n")?; | |
737 | } | |
738 | } else { | |
739 | let mut out = std::io::stdout(); | |
740 | out.write_all(&last.catalog.name)?; | |
741 | out.write_all(b"\n")?; | |
742 | } | |
743 | Ok(()) | |
fee5528e CE |
744 | } |
745 | ||
c443f58b WB |
746 | async fn stat(&mut self, path: PathBuf) -> Result<(), Error> { |
747 | let mut stack = Self::lookup( | |
748 | &self.position, | |
749 | &mut self.catalog, | |
750 | &self.accessor, | |
751 | Some(&path), | |
752 | &mut Some(0), | |
753 | ) | |
754 | .await?; | |
755 | ||
756 | let file = Self::walk_pxar_archive(&self.accessor, &mut stack).await?; | |
757 | std::io::stdout() | |
758 | .write_all(crate::pxar::format_multi_line_entry(file.entry()).as_bytes())?; | |
759 | Ok(()) | |
760 | } | |
761 | ||
762 | async fn cd(&mut self, path: Option<&Path>) -> Result<(), Error> { | |
763 | match path { | |
764 | Some(path) => { | |
765 | let new_position = Self::lookup( | |
766 | &self.position, | |
767 | &mut self.catalog, | |
768 | &self.accessor, | |
769 | Some(path), | |
770 | &mut None, | |
771 | ) | |
772 | .await?; | |
773 | if !new_position.last().unwrap().catalog.is_directory() { | |
774 | bail!("not a directory"); | |
775 | } | |
776 | self.position = new_position; | |
777 | } | |
778 | None => self.position.truncate(1), | |
779 | } | |
780 | self.update_prompt(); | |
781 | Ok(()) | |
782 | } | |
783 | ||
784 | /// This stack must have been canonicalized already! | |
785 | fn format_path_stack(stack: &[PathStackEntry]) -> OsString { | |
786 | if stack.len() <= 1 { | |
787 | return OsString::from("/"); | |
788 | } | |
789 | ||
790 | let mut out = OsString::new(); | |
791 | for c in stack.iter().skip(1) { | |
792 | out.push("/"); | |
793 | out.push(OsStr::from_bytes(&c.catalog.name)); | |
794 | } | |
795 | ||
796 | out | |
797 | } | |
798 | ||
799 | async fn select(&mut self, path: PathBuf) -> Result<(), Error> { | |
800 | let stack = Self::lookup( | |
801 | &self.position, | |
802 | &mut self.catalog, | |
803 | &self.accessor, | |
804 | Some(&path), | |
805 | &mut Some(0), | |
806 | ) | |
807 | .await?; | |
808 | ||
809 | let path = Self::format_path_stack(&stack); | |
810 | let entry = MatchEntry::include(MatchPattern::Literal(path.as_bytes().to_vec())); | |
811 | if self.selected.insert(path.clone(), entry).is_some() { | |
812 | println!("path already selected: {:?}", path); | |
813 | } else { | |
814 | println!("added path: {:?}", path); | |
815 | } | |
816 | ||
817 | Ok(()) | |
fee5528e CE |
818 | } |
819 | ||
c443f58b WB |
820 | async fn deselect(&mut self, path: PathBuf) -> Result<(), Error> { |
821 | let stack = Self::lookup( | |
822 | &self.position, | |
823 | &mut self.catalog, | |
824 | &self.accessor, | |
825 | Some(&path), | |
826 | &mut Some(0), | |
827 | ) | |
828 | .await?; | |
829 | ||
830 | let path = Self::format_path_stack(&stack); | |
831 | ||
832 | if self.selected.remove(&path).is_some() { | |
833 | println!("removed path from selection: {:?}", path); | |
834 | } else { | |
835 | println!("path not selected: {:?}", path); | |
836 | } | |
837 | ||
838 | Ok(()) | |
fee5528e CE |
839 | } |
840 | ||
c443f58b WB |
841 | async fn deselect_all(&mut self) -> Result<(), Error> { |
842 | self.selected.clear(); | |
843 | println!("cleared selection"); | |
844 | Ok(()) | |
fee5528e CE |
845 | } |
846 | ||
c443f58b WB |
847 | async fn list_selected(&mut self, patterns: bool) -> Result<(), Error> { |
848 | if patterns { | |
849 | self.list_selected_patterns().await | |
fee5528e | 850 | } else { |
c443f58b | 851 | self.list_matching_files().await |
fee5528e CE |
852 | } |
853 | } | |
854 | ||
c443f58b WB |
855 | async fn list_selected_patterns(&self) -> Result<(), Error> { |
856 | for entry in self.selected.keys() { | |
857 | println!("{:?}", entry); | |
858 | } | |
859 | Ok(()) | |
fee5528e CE |
860 | } |
861 | ||
c443f58b WB |
862 | fn build_match_list(&self) -> Vec<MatchEntry> { |
863 | let mut list = Vec::with_capacity(self.selected.len()); | |
864 | for entry in self.selected.values() { | |
865 | list.push(entry.clone()); | |
866 | } | |
867 | list | |
fee5528e CE |
868 | } |
869 | ||
c443f58b WB |
870 | async fn list_matching_files(&mut self) -> Result<(), Error> { |
871 | let matches = self.build_match_list(); | |
872 | ||
873 | self.catalog.find( | |
874 | &self.position[0].catalog, | |
875 | &mut Vec::new(), | |
876 | &matches, | |
877 | &mut |path: &[u8]| -> Result<(), Error> { | |
878 | let mut out = std::io::stdout(); | |
879 | out.write_all(path)?; | |
880 | out.write_all(b"\n")?; | |
881 | Ok(()) | |
882 | }, | |
883 | )?; | |
884 | ||
885 | Ok(()) | |
886 | } | |
887 | ||
888 | async fn find(&mut self, pattern: String, select: bool) -> Result<(), Error> { | |
889 | let pattern_os = OsString::from(pattern.clone()); | |
890 | let pattern_entry = | |
891 | MatchEntry::parse_pattern(pattern, PatternFlag::PATH_NAME, MatchType::Include)?; | |
892 | ||
893 | let mut found_some = false; | |
894 | self.catalog.find( | |
895 | &self.position[0].catalog, | |
896 | &mut Vec::new(), | |
897 | &[&pattern_entry], | |
898 | &mut |path: &[u8]| -> Result<(), Error> { | |
899 | found_some = true; | |
900 | let mut out = std::io::stdout(); | |
901 | out.write_all(path)?; | |
902 | out.write_all(b"\n")?; | |
903 | Ok(()) | |
904 | }, | |
905 | )?; | |
906 | ||
907 | if found_some && select { | |
908 | self.selected.insert(pattern_os, pattern_entry); | |
909 | } | |
910 | ||
911 | Ok(()) | |
912 | } | |
913 | ||
914 | async fn restore_selected(&mut self, destination: PathBuf) -> Result<(), Error> { | |
915 | if self.selected.is_empty() { | |
916 | bail!("no entries selected"); | |
917 | } | |
918 | ||
919 | let match_list = self.build_match_list(); | |
920 | ||
921 | self.restore_with_match_list(destination, &match_list).await | |
922 | } | |
923 | ||
924 | async fn restore( | |
fee5528e | 925 | &mut self, |
c443f58b WB |
926 | destination: PathBuf, |
927 | pattern: Option<String>, | |
fee5528e | 928 | ) -> Result<(), Error> { |
c443f58b WB |
929 | let tmp; |
930 | let match_list: &[MatchEntry] = match pattern { | |
931 | None => &[], | |
932 | Some(pattern) => { | |
933 | tmp = [MatchEntry::parse_pattern( | |
934 | pattern, | |
935 | PatternFlag::PATH_NAME, | |
936 | MatchType::Include, | |
937 | )?]; | |
938 | &tmp | |
939 | } | |
940 | }; | |
941 | ||
942 | self.restore_with_match_list(destination, match_list).await | |
943 | } | |
944 | ||
945 | async fn restore_with_match_list( | |
946 | &mut self, | |
947 | destination: PathBuf, | |
948 | match_list: &[MatchEntry], | |
949 | ) -> Result<(), Error> { | |
950 | create_path( | |
951 | &destination, | |
952 | None, | |
953 | Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))), | |
954 | ) | |
955 | .map_err(|err| format_err!("error creating directory {:?}: {}", destination, err))?; | |
956 | ||
957 | let rootdir = Dir::open( | |
958 | &destination, | |
959 | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC, | |
960 | Mode::empty(), | |
961 | ) | |
962 | .map_err(|err| { | |
963 | format_err!("unable to open target directory {:?}: {}", destination, err,) | |
964 | })?; | |
965 | ||
966 | let mut dir_stack = self.new_path_stack(); | |
967 | Self::walk_pxar_archive(&self.accessor, &mut dir_stack).await?; | |
968 | let root_meta = dir_stack | |
969 | .last() | |
970 | .unwrap() | |
971 | .pxar | |
972 | .as_ref() | |
973 | .unwrap() | |
974 | .entry() | |
975 | .metadata() | |
976 | .clone(); | |
977 | let pxar_dir_stack = PxarDirStack::new(rootdir, root_meta); | |
978 | ||
979 | let mut extractor = ExtractorState::new( | |
5444fa94 | 980 | Flags::DEFAULT, |
c443f58b WB |
981 | &mut self.catalog, |
982 | dir_stack, | |
983 | pxar_dir_stack, | |
984 | &match_list, | |
985 | &self.accessor, | |
986 | )?; | |
987 | ||
988 | extractor.extract().await | |
989 | } | |
990 | } | |
991 | ||
992 | enum LoopState { | |
993 | Break, | |
994 | Continue, | |
995 | } | |
996 | ||
997 | struct ExtractorState<'a> { | |
998 | path: Vec<u8>, | |
999 | path_len: usize, | |
1000 | path_len_stack: Vec<usize>, | |
1001 | ||
1002 | dir_stack: Vec<PathStackEntry>, | |
1003 | ||
1004 | matches: bool, | |
1005 | matches_stack: Vec<bool>, | |
1006 | ||
1007 | read_dir: <Vec<catalog::DirEntry> as IntoIterator>::IntoIter, | |
1008 | read_dir_stack: Vec<<Vec<catalog::DirEntry> as IntoIterator>::IntoIter>, | |
1009 | ||
1010 | pxar_dir_stack: PxarDirStack, | |
1011 | ||
1012 | catalog: &'a mut CatalogReader, | |
5444fa94 | 1013 | feature_flags: Flags, |
c443f58b WB |
1014 | match_list: &'a [MatchEntry], |
1015 | accessor: &'a Accessor, | |
1016 | } | |
1017 | ||
1018 | impl<'a> ExtractorState<'a> { | |
1019 | pub fn new( | |
5444fa94 | 1020 | feature_flags: Flags, |
c443f58b WB |
1021 | catalog: &'a mut CatalogReader, |
1022 | dir_stack: Vec<PathStackEntry>, | |
1023 | pxar_dir_stack: PxarDirStack, | |
1024 | match_list: &'a [MatchEntry], | |
1025 | accessor: &'a Accessor, | |
1026 | ) -> Result<Self, Error> { | |
1027 | let read_dir = catalog | |
1028 | .read_dir(&dir_stack.last().unwrap().catalog)? | |
1029 | .into_iter(); | |
1030 | Ok(Self { | |
1031 | path: Vec::new(), | |
1032 | path_len: 0, | |
1033 | path_len_stack: Vec::new(), | |
1034 | ||
1035 | dir_stack, | |
1036 | ||
1037 | matches: match_list.is_empty(), | |
1038 | matches_stack: Vec::new(), | |
1039 | ||
1040 | read_dir, | |
1041 | read_dir_stack: Vec::new(), | |
1042 | ||
1043 | pxar_dir_stack, | |
1044 | ||
1045 | catalog, | |
1046 | feature_flags, | |
1047 | match_list, | |
1048 | accessor, | |
1049 | }) | |
1050 | } | |
1051 | ||
1052 | pub async fn extract(&mut self) -> Result<(), Error> { | |
1053 | loop { | |
1054 | let entry = match self.read_dir.next() { | |
1055 | Some(entry) => entry, | |
1056 | None => match self.handle_end_of_directory()? { | |
1057 | LoopState::Break => break, // done with root directory | |
1058 | LoopState::Continue => continue, | |
1059 | }, | |
1060 | }; | |
1061 | ||
1062 | self.path.truncate(self.path_len); | |
1063 | if !entry.name.starts_with(b"/") { | |
1064 | self.path.reserve(entry.name.len() + 1); | |
1065 | self.path.push(b'/'); | |
fee5528e | 1066 | } |
c443f58b WB |
1067 | self.path.extend(&entry.name); |
1068 | ||
1069 | self.handle_entry(entry).await?; | |
fee5528e | 1070 | } |
c443f58b WB |
1071 | |
1072 | Ok(()) | |
1073 | } | |
1074 | ||
1075 | fn handle_end_of_directory(&mut self) -> Result<LoopState, Error> { | |
1076 | // go up a directory: | |
1077 | self.read_dir = match self.read_dir_stack.pop() { | |
1078 | Some(r) => r, | |
1079 | None => return Ok(LoopState::Break), // out of root directory | |
1080 | }; | |
1081 | ||
1082 | self.matches = self | |
1083 | .matches_stack | |
1084 | .pop() | |
1085 | .ok_or_else(|| format_err!("internal iterator error (matches_stack)"))?; | |
1086 | ||
1087 | self.dir_stack | |
1088 | .pop() | |
1089 | .ok_or_else(|| format_err!("internal iterator error (dir_stack)"))?; | |
1090 | ||
1091 | let dir = self | |
1092 | .pxar_dir_stack | |
1093 | .pop()? | |
1094 | .ok_or_else(|| format_err!("internal iterator error (pxar_dir_stack)"))?; | |
1095 | ||
1096 | self.path_len = self | |
1097 | .path_len_stack | |
1098 | .pop() | |
1099 | .ok_or_else(|| format_err!("internal iterator error (path_len_stack)"))?; | |
1100 | ||
1101 | self.path.push(0); | |
1102 | let dirname = CStr::from_bytes_with_nul(&self.path[(self.path_len + 1)..])?; | |
1103 | ||
1104 | if let Some(fd) = dir.try_as_raw_fd() { | |
1105 | // the directory was created, so apply the metadata: | |
1106 | metadata::apply(self.feature_flags, dir.metadata(), fd, dirname)?; | |
fee5528e | 1107 | } |
c443f58b WB |
1108 | |
1109 | Ok(LoopState::Continue) | |
1110 | } | |
1111 | ||
1112 | async fn handle_new_directory( | |
1113 | &mut self, | |
1114 | entry: catalog::DirEntry, | |
1115 | match_result: Option<MatchType>, | |
1116 | ) -> Result<(), Error> { | |
1117 | // enter a new directory: | |
1118 | self.read_dir_stack.push(mem::replace( | |
1119 | &mut self.read_dir, | |
1120 | self.catalog.read_dir(&entry)?.into_iter(), | |
1121 | )); | |
1122 | self.matches_stack.push(self.matches); | |
1123 | self.dir_stack.push(PathStackEntry::new(entry)); | |
1124 | self.path_len_stack.push(self.path_len); | |
1125 | self.path_len = self.path.len(); | |
1126 | ||
1127 | Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?; | |
1128 | let dir_pxar = self.dir_stack.last().unwrap().pxar.as_ref().unwrap(); | |
1129 | let dir_meta = dir_pxar.entry().metadata().clone(); | |
1130 | self.pxar_dir_stack | |
1131 | .push(dir_pxar.file_name().to_os_string(), dir_meta)?; | |
1132 | ||
1133 | if self.matches && match_result != Some(MatchType::Exclude) { | |
1134 | todo!("create this directory"); | |
1135 | } | |
1136 | ||
fee5528e CE |
1137 | Ok(()) |
1138 | } | |
1139 | ||
c443f58b WB |
1140 | pub async fn handle_entry(&mut self, entry: catalog::DirEntry) -> Result<(), Error> { |
1141 | let match_result = self.match_list.matches(&self.path, entry.get_file_mode()); | |
1142 | let did_match = match match_result { | |
1143 | Some(MatchType::Include) => true, | |
1144 | Some(MatchType::Exclude) => false, | |
1145 | None => self.matches, | |
1146 | }; | |
1147 | ||
1148 | match (did_match, &entry.attr) { | |
1149 | (_, DirEntryAttribute::Directory { .. }) => { | |
1150 | self.handle_new_directory(entry, match_result).await?; | |
1151 | } | |
1152 | (true, DirEntryAttribute::File { .. }) => { | |
1153 | self.dir_stack.push(PathStackEntry::new(entry)); | |
1154 | let file = Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?; | |
1155 | self.extract_file(file).await?; | |
1156 | self.dir_stack.pop(); | |
1157 | } | |
1158 | (true, DirEntryAttribute::Symlink) | |
1159 | | (true, DirEntryAttribute::BlockDevice) | |
1160 | | (true, DirEntryAttribute::CharDevice) | |
1161 | | (true, DirEntryAttribute::Fifo) | |
1162 | | (true, DirEntryAttribute::Socket) | |
1163 | | (true, DirEntryAttribute::Hardlink) => { | |
1164 | let attr = entry.attr.clone(); | |
1165 | self.dir_stack.push(PathStackEntry::new(entry)); | |
1166 | let file = Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?; | |
1167 | self.extract_special(file, attr).await?; | |
1168 | self.dir_stack.pop(); | |
1169 | } | |
1170 | (false, _) => (), // skip | |
1171 | } | |
1172 | ||
1173 | Ok(()) | |
1174 | } | |
1175 | ||
1176 | fn path(&self) -> &OsStr { | |
1177 | OsStr::from_bytes(&self.path) | |
1178 | } | |
1179 | ||
1180 | async fn extract_file(&mut self, entry: FileEntry) -> Result<(), Error> { | |
1181 | match entry.kind() { | |
1182 | pxar::EntryKind::File { size, .. } => { | |
1183 | let mut contents = entry.contents().await?; | |
1184 | ||
1185 | let parent = self.pxar_dir_stack.last_dir_fd(true)?; | |
1186 | let mut file = tokio::fs::File::from_std(unsafe { | |
1187 | std::fs::File::from_raw_fd(nix::fcntl::openat( | |
1188 | parent, | |
1189 | entry.file_name(), | |
6988b29b | 1190 | OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_WRONLY | OFlag::O_CLOEXEC, |
c443f58b WB |
1191 | Mode::from_bits(0o600).unwrap(), |
1192 | )?) | |
1193 | }); | |
1194 | ||
1195 | let extracted = tokio::io::copy(&mut contents, &mut file).await?; | |
1196 | if *size != extracted { | |
1197 | bail!("extracted {} bytes of a file of {} bytes", extracted, size); | |
fee5528e | 1198 | } |
c443f58b WB |
1199 | |
1200 | metadata::apply_with_path( | |
5444fa94 | 1201 | Flags::DEFAULT, |
c443f58b WB |
1202 | entry.metadata(), |
1203 | file.as_raw_fd(), | |
1204 | entry.file_name(), | |
1205 | )?; | |
1206 | ||
1207 | Ok(()) | |
1208 | } | |
1209 | _ => { | |
1210 | bail!( | |
1211 | "catalog file {:?} not a regular file in the archive", | |
1212 | self.path() | |
1213 | ); | |
fee5528e CE |
1214 | } |
1215 | } | |
fee5528e CE |
1216 | } |
1217 | ||
c443f58b WB |
1218 | async fn extract_special( |
1219 | &mut self, | |
1220 | entry: FileEntry, | |
1221 | catalog_attr: DirEntryAttribute, | |
1222 | ) -> Result<(), Error> { | |
1223 | match (catalog_attr, entry.kind()) { | |
1224 | (DirEntryAttribute::Symlink, pxar::EntryKind::Symlink(symlink)) => { | |
1225 | self.extract_symlink(entry.file_name(), symlink.as_os_str(), entry.metadata()) | |
1226 | } | |
1227 | (DirEntryAttribute::Symlink, _) => { | |
1228 | bail!( | |
1229 | "catalog symlink {:?} not a symlink in the archive", | |
1230 | self.path() | |
1231 | ); | |
1232 | } | |
1233 | ||
1234 | (DirEntryAttribute::Hardlink, pxar::EntryKind::Hardlink(hardlink)) => { | |
1235 | self.extract_hardlink(entry.file_name(), hardlink.as_os_str(), entry.metadata()) | |
1236 | } | |
1237 | (DirEntryAttribute::Hardlink, _) => { | |
1238 | bail!( | |
1239 | "catalog hardlink {:?} not a hardlink in the archive", | |
1240 | self.path() | |
1241 | ); | |
fee5528e | 1242 | } |
c443f58b WB |
1243 | |
1244 | (ref attr, pxar::EntryKind::Device(device)) => { | |
1245 | self.extract_device(attr.clone(), entry.file_name(), device, entry.metadata()) | |
1246 | } | |
1247 | ||
1248 | (DirEntryAttribute::Fifo, pxar::EntryKind::Fifo) => { | |
1249 | self.extract_node(entry.file_name(), 0, entry.metadata()) | |
1250 | } | |
1251 | (DirEntryAttribute::Fifo, _) => { | |
1252 | bail!("catalog fifo {:?} not a fifo in the archive", self.path()); | |
1253 | } | |
1254 | ||
1255 | (DirEntryAttribute::Socket, pxar::EntryKind::Socket) => { | |
1256 | self.extract_node(entry.file_name(), 0, entry.metadata()) | |
1257 | } | |
1258 | (DirEntryAttribute::Socket, _) => { | |
1259 | bail!( | |
1260 | "catalog socket {:?} not a socket in the archive", | |
1261 | self.path() | |
1262 | ); | |
1263 | } | |
1264 | ||
1265 | attr => bail!("unhandled file type {:?} for {:?}", attr, self.path()), | |
fee5528e | 1266 | } |
fee5528e CE |
1267 | } |
1268 | ||
c443f58b WB |
1269 | fn extract_symlink( |
1270 | &mut self, | |
1271 | file_name: &OsStr, | |
1272 | target: &OsStr, | |
1273 | metadata: &Metadata, | |
1274 | ) -> Result<(), Error> { | |
1275 | let parent = self.pxar_dir_stack.last_dir_fd(true)?; | |
1276 | nix::unistd::symlinkat(target, Some(parent), file_name)?; | |
1277 | ||
1278 | metadata::apply_at( | |
1279 | self.feature_flags, | |
1280 | metadata, | |
1281 | parent, | |
1282 | &CString::new(file_name.as_bytes())?, | |
1283 | )?; | |
1284 | ||
1285 | Ok(()) | |
1286 | } | |
1287 | ||
1288 | fn extract_hardlink( | |
1289 | &mut self, | |
1290 | file_name: &OsStr, | |
1291 | target: &OsStr, | |
1292 | _metadata: &Metadata, | |
1293 | ) -> Result<(), Error> { | |
1294 | crate::pxar::tools::assert_relative_path(target)?; | |
1295 | ||
1296 | let parent = self.pxar_dir_stack.last_dir_fd(true)?; | |
1297 | let root = self.pxar_dir_stack.root_dir_fd()?; | |
1298 | nix::unistd::linkat( | |
1299 | Some(root), | |
1300 | target, | |
1301 | Some(parent), | |
1302 | file_name, | |
1303 | nix::unistd::LinkatFlags::NoSymlinkFollow, | |
1304 | )?; | |
1305 | ||
1306 | Ok(()) | |
1307 | } | |
1308 | ||
1309 | fn extract_device( | |
1310 | &mut self, | |
1311 | attr: DirEntryAttribute, | |
1312 | file_name: &OsStr, | |
1313 | device: &pxar::format::Device, | |
1314 | metadata: &Metadata, | |
1315 | ) -> Result<(), Error> { | |
1316 | match attr { | |
1317 | DirEntryAttribute::BlockDevice => { | |
1318 | if !metadata.stat.is_blockdev() { | |
1319 | bail!( | |
1320 | "catalog block device {:?} is not a block device in the archive", | |
1321 | self.path(), | |
1322 | ); | |
fee5528e | 1323 | } |
c443f58b WB |
1324 | } |
1325 | DirEntryAttribute::CharDevice => { | |
1326 | if !metadata.stat.is_chardev() { | |
1327 | bail!( | |
1328 | "catalog character device {:?} is not a character device in the archive", | |
1329 | self.path(), | |
1330 | ); | |
fee5528e CE |
1331 | } |
1332 | } | |
c443f58b WB |
1333 | _ => { |
1334 | bail!( | |
1335 | "unexpected file type for {:?} in the catalog, \ | |
1336 | which is a device special file in the archive", | |
1337 | self.path(), | |
1338 | ); | |
1339 | } | |
fee5528e | 1340 | } |
c443f58b WB |
1341 | self.extract_node(file_name, device.to_dev_t(), metadata) |
1342 | } | |
1343 | ||
1344 | fn extract_node( | |
1345 | &mut self, | |
1346 | file_name: &OsStr, | |
1347 | device: libc::dev_t, | |
1348 | metadata: &Metadata, | |
1349 | ) -> Result<(), Error> { | |
1350 | let mode = metadata.stat.mode; | |
1351 | let mode = u32::try_from(mode).map_err(|_| { | |
1352 | format_err!( | |
1353 | "device node's mode contains illegal bits: 0x{:x} (0o{:o})", | |
1354 | mode, | |
1355 | mode, | |
1356 | ) | |
1357 | })?; | |
1358 | ||
1359 | let parent = self.pxar_dir_stack.last_dir_fd(true)?; | |
1360 | let file_name = CString::new(file_name.as_bytes())?; | |
1361 | unsafe { c_result!(libc::mknodat(parent, file_name.as_ptr(), mode, device)) } | |
1362 | .map_err(|err| format_err!("failed to create device node: {}", err))?; | |
1363 | ||
1364 | metadata::apply_at(self.feature_flags, metadata, parent, &file_name) | |
fee5528e CE |
1365 | } |
1366 | } |