]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/backup/catalog_shell.rs
turn pxar::flags into bitflags, pxar::Flags
[proxmox-backup.git] / src / backup / catalog_shell.rs
index b79a3a7ce859751aac734b042e872bdd1a076f09..87fede0c3e3625e24e5e1720831d751e9342d8c8 100644 (file)
-use std::cell::RefCell;
 use std::collections::HashMap;
-use std::ffi::{CString, OsStr};
+use std::convert::TryFrom;
+use std::ffi::{CStr, CString, OsStr, OsString};
+use std::future::Future;
 use std::io::Write;
+use std::mem;
 use std::os::unix::ffi::OsStrExt;
-use std::path::Path;
+use std::os::unix::io::{AsRawFd, FromRawFd};
+use std::path::{Path, PathBuf};
+use std::pin::Pin;
 
-use failure::*;
+use anyhow::{bail, format_err, Error};
+use nix::dir::Dir;
+use nix::fcntl::OFlag;
+use nix::sys::stat::Mode;
 
-use super::catalog::{CatalogReader, DirEntry};
-use crate::pxar::*;
-use crate::tools;
+use pathpatterns::{MatchEntry, MatchList, MatchPattern, MatchType, PatternFlag};
+use proxmox::api::api;
+use proxmox::api::cli::{self, CliCommand, CliCommandMap, CliHelper, CommandLineInterface};
+use proxmox::c_result;
+use proxmox::tools::fs::{create_path, CreateOptions};
+use pxar::{EntryKind, Metadata};
 
-use proxmox::api::{cli::*, *};
+use crate::backup::catalog::{self, DirEntryAttribute};
 
-const PROMPT_PREFIX: &str = "pxar:";
-const PROMPT: &str = ">";
+// FIXME: Remove looku_self() calls by putting Directory into the dir stack
+use crate::pxar::dir_stack::PxarDirStack;
+use crate::pxar::Flags;
+use crate::pxar::fuse::{Accessor, FileEntry};
+use crate::pxar::metadata;
 
-/// Interactive shell for interacton with the catalog.
-pub struct Shell {
-    /// Readline instance handling input and callbacks
-    rl: rustyline::Editor<CliHelper>,
-    prompt: String,
-}
+type CatalogReader = crate::backup::CatalogReader<std::fs::File>;
+
+const MAX_SYMLINK_COUNT: usize = 40;
+
+static mut SHELL: Option<usize> = None;
 
 /// This list defines all the shell commands and their properties
 /// using the api schema
 pub fn catalog_shell_cli() -> CommandLineInterface {
-
-    let map = CliCommandMap::new()
-        .insert("pwd", CliCommand::new(&API_METHOD_PWD_COMMAND))
-        .insert(
-            "cd",
-            CliCommand::new(&API_METHOD_CD_COMMAND)
-                .arg_param(&["path"])
-                .completion_cb("path", Shell::complete_path)
-        )
-        .insert(
-            "ls",
-            CliCommand::new(&API_METHOD_LS_COMMAND)
-                .arg_param(&["path"])
-                .completion_cb("path", Shell::complete_path)
-         )
-        .insert(
-            "stat",
-            CliCommand::new(&API_METHOD_STAT_COMMAND)
-                .arg_param(&["path"])
-                .completion_cb("path", Shell::complete_path)
-         )
-        .insert(
-            "select",
-            CliCommand::new(&API_METHOD_SELECT_COMMAND)
-                .arg_param(&["path"])
-                .completion_cb("path", Shell::complete_path)
-        )
-        .insert(
-            "deselect",
-            CliCommand::new(&API_METHOD_DESELECT_COMMAND)
-                .arg_param(&["path"])
-                .completion_cb("path", Shell::complete_path)
-        )
-        .insert(
-            "restore-selected",
-            CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND)
-                .arg_param(&["target"])
-                .completion_cb("target", tools::complete_file_name)
-        )
-        .insert(
-            "list-selected",
-            CliCommand::new(&API_METHOD_LIST_SELECTED_COMMAND),
-        )
-        .insert(
-            "restore",
-            CliCommand::new(&API_METHOD_RESTORE_COMMAND)
-                .arg_param(&["target"])
-                .completion_cb("target", tools::complete_file_name)
-        )
-        .insert(
-            "find",
-            CliCommand::new(&API_METHOD_FIND_COMMAND)
-                .arg_param(&["path", "pattern"])
-                .completion_cb("path", Shell::complete_path)
-        )
-        .insert_help();
-
-    CommandLineInterface::Nested(map)
+    CommandLineInterface::Nested(
+        CliCommandMap::new()
+            .insert("pwd", CliCommand::new(&API_METHOD_PWD_COMMAND))
+            .insert(
+                "cd",
+                CliCommand::new(&API_METHOD_CD_COMMAND)
+                    .arg_param(&["path"])
+                    .completion_cb("path", complete_path),
+            )
+            .insert(
+                "ls",
+                CliCommand::new(&API_METHOD_LS_COMMAND)
+                    .arg_param(&["path"])
+                    .completion_cb("path", complete_path),
+            )
+            .insert(
+                "stat",
+                CliCommand::new(&API_METHOD_STAT_COMMAND)
+                    .arg_param(&["path"])
+                    .completion_cb("path", complete_path),
+            )
+            .insert(
+                "select",
+                CliCommand::new(&API_METHOD_SELECT_COMMAND)
+                    .arg_param(&["path"])
+                    .completion_cb("path", complete_path),
+            )
+            .insert(
+                "deselect",
+                CliCommand::new(&API_METHOD_DESELECT_COMMAND)
+                    .arg_param(&["path"])
+                    .completion_cb("path", complete_path),
+            )
+            .insert(
+                "clear-selected",
+                CliCommand::new(&API_METHOD_CLEAR_SELECTED_COMMAND),
+            )
+            .insert(
+                "list-selected",
+                CliCommand::new(&API_METHOD_LIST_SELECTED_COMMAND),
+            )
+            .insert(
+                "restore-selected",
+                CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND)
+                    .arg_param(&["target"])
+                    .completion_cb("target", crate::tools::complete_file_name),
+            )
+            .insert(
+                "restore",
+                CliCommand::new(&API_METHOD_RESTORE_COMMAND)
+                    .arg_param(&["target"])
+                    .completion_cb("target", crate::tools::complete_file_name),
+            )
+            .insert(
+                "find",
+                CliCommand::new(&API_METHOD_FIND_COMMAND).arg_param(&["pattern"]),
+            )
+            .insert_help(),
+    )
 }
 
-impl Shell {
-    /// Create a new shell for the given catalog and pxar archive.
-    pub fn new(
-        mut catalog: CatalogReader<std::fs::File>,
-        archive_name: &str,
-        decoder: Decoder,
-    ) -> Result<Self, Error> {
-        let catalog_root = catalog.root()?;
-        // The root for the given archive as stored in the catalog
-        let archive_root = catalog.lookup(&catalog_root, archive_name.as_bytes())?;
-        let root = vec![archive_root];
-
-        CONTEXT.with(|handle| {
-            let mut ctx = handle.borrow_mut();
-            *ctx = Some(Context {
-                catalog,
-                selected: Vec::new(),
-                decoder,
-                root: root.clone(),
-                current: root,
-            });
-        });
-
-        let cli_helper = CliHelper::new(catalog_shell_cli());
-        let mut rl = rustyline::Editor::<CliHelper>::new();
-        rl.set_helper(Some(cli_helper));
-
-        Context::with(|ctx| {
-            Ok(Self {
-                rl,
-                prompt: ctx.generate_prompt()?,
-            })
-        })
-    }
-
-    /// Start the interactive shell loop
-    pub fn shell(mut self) -> Result<(), Error> {
-        while let Ok(line) = self.rl.readline(&self.prompt) {
-            let helper = self.rl.helper().unwrap();
-            let args = match shellword_split(&line) {
-                Ok(args) => args,
-                Err(err) => {
-                    println!("Error: {}", err);
-                    continue;
-                }
-            };
-            let _ = handle_command(helper.cmd_def(), "", args);
-            self.rl.add_history_entry(line);
-            self.update_prompt()?;
+fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> {
+    let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) };
+    match shell.complete_path(complete_me) {
+        Ok(list) => list,
+        Err(err) => {
+            eprintln!("error during completion: {}", err);
+            Vec::new()
         }
-        Ok(())
-    }
-
-    /// Update the prompt to the new working directory
-    fn update_prompt(&mut self) -> Result<(), Error> {
-        Context::with(|ctx| {
-            self.prompt = ctx.generate_prompt()?;
-            Ok(())
-        })
-    }
-
-    /// Completions for paths by lookup in the catalog
-    fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> {
-        Context::with(|ctx| {
-            let (base, to_complete) = match complete_me.rfind('/') {
-                // Split at ind + 1 so the slash remains on base, ok also if
-                // ends in slash as split_at accepts up to length as index.
-                Some(ind) => complete_me.split_at(ind + 1),
-                None => ("", complete_me),
-            };
-
-            let current = if base.is_empty() {
-                ctx.current.clone()
-            } else {
-                ctx.canonical_path(base)?
-            };
-
-            let entries = match ctx.catalog.read_dir(&current.last().unwrap()) {
-                Ok(entries) => entries,
-                Err(_) => return Ok(Vec::new()),
-            };
-
-            let mut list = Vec::new();
-            for entry in &entries {
-                let mut name = String::from(base);
-                if entry.name.starts_with(to_complete.as_bytes()) {
-                    name.push_str(std::str::from_utf8(&entry.name)?);
-                    if entry.is_directory() {
-                        name.push('/');
-                    }
-                    list.push(name);
-                }
-            }
-            Ok(list)
-        })
-        .unwrap_or_default()
     }
 }
 
 #[api(input: { properties: {} })]
 /// List the current working directory.
-fn pwd_command() -> Result<(), Error> {
-    Context::with(|ctx| {
-        let path = Context::generate_cstring(&ctx.current)?;
-        let mut out = std::io::stdout();
-        out.write_all(&path.as_bytes())?;
-        out.write_all(&[b'\n'])?;
-        out.flush()?;
-        Ok(())
-    })
+async fn pwd_command() -> Result<(), Error> {
+    Shell::with(move |shell| shell.pwd()).await
 }
 
 #[api(
@@ -210,22 +128,9 @@ fn pwd_command() -> Result<(), Error> {
     }
 )]
 /// Change the current working directory to the new directory
-fn cd_command(path: Option<String>) -> Result<(), Error> {
-    Context::with(|ctx| {
-        let path = path.unwrap_or_default();
-        let mut path = ctx.canonical_path(&path)?;
-        if !path
-            .last()
-            .ok_or_else(|| format_err!("invalid path component"))?
-            .is_directory()
-        {
-            // Change to the parent dir of the file instead
-            path.pop();
-            eprintln!("not a directory, fallback to parent directory");
-        }
-        ctx.current = path;
-        Ok(())
-    })
+async fn cd_command(path: Option<String>) -> Result<(), Error> {
+    let path = path.as_ref().map(Path::new);
+    Shell::with(move |shell| shell.cd(path)).await
 }
 
 #[api(
@@ -240,51 +145,9 @@ fn cd_command(path: Option<String>) -> Result<(), Error> {
     }
 )]
 /// List the content of working directory or given path.
-fn ls_command(path: Option<String>) -> Result<(), Error> {
-    Context::with(|ctx| {
-        let parent = if let Some(path) = path {
-            ctx.canonical_path(&path)?
-                .last()
-                .ok_or_else(|| format_err!("invalid path component"))?
-                .clone()
-        } else {
-            ctx.current.last().unwrap().clone()
-        };
-
-        let list = if parent.is_directory() {
-            ctx.catalog.read_dir(&parent)?
-        } else {
-            vec![parent]
-        };
-
-        if list.is_empty() {
-            return Ok(());
-        }
-        let max = list.iter().max_by(|x, y| x.name.len().cmp(&y.name.len()));
-        let max = match max {
-            Some(dir_entry) => dir_entry.name.len() + 1,
-            None => 0,
-        };
-
-        let (_rows, mut cols) = Context::get_terminal_size();
-        cols /= max;
-
-        let mut out = std::io::stdout();
-        for (index, item) in list.iter().enumerate() {
-            out.write_all(&item.name)?;
-            // Fill with whitespaces
-            out.write_all(&vec![b' '; max - item.name.len()])?;
-            if index % cols == (cols - 1) {
-                out.write_all(&[b'\n'])?;
-            }
-        }
-        // If the last line is not complete, add the newline
-        if list.len() % cols != cols - 1 {
-            out.write_all(&[b'\n'])?;
-        }
-        out.flush()?;
-        Ok(())
-    })
+async fn ls_command(path: Option<String>) -> Result<(), Error> {
+    let path = path.as_ref().map(Path::new);
+    Shell::with(move |shell| shell.ls(path)).await
 }
 
 #[api(
@@ -299,36 +162,10 @@ fn ls_command(path: Option<String>) -> Result<(), Error> {
 )]
 /// Read the metadata for a given directory entry.
 ///
-/// This is expensive because the data has to be read from the pxar `Decoder`,
-/// which means reading over the network.
-fn stat_command(path: String) -> Result<(), Error> {
-    Context::with(|ctx| {
-        // First check if the file exists in the catalog, therefore avoiding
-        // expensive calls to the decoder just to find out that there maybe is
-        // no such entry.
-        // This is done by calling canonical_path(), which returns the full path
-        // if it exists, error otherwise.
-        let path = ctx.canonical_path(&path)?;
-        let (item, _attr, size) = ctx.lookup(&path)?;
-        let mut out = std::io::stdout();
-        out.write_all(b"File: ")?;
-        out.write_all(item.filename.as_bytes())?;
-        out.write_all(&[b'\n'])?;
-        out.write_all(format!("Size: {}\n", size).as_bytes())?;
-        out.write_all(b"Type: ")?;
-        match item.entry.mode as u32 & libc::S_IFMT {
-            libc::S_IFDIR => out.write_all(b"directory\n")?,
-            libc::S_IFREG => out.write_all(b"regular file\n")?,
-            libc::S_IFLNK => out.write_all(b"symbolic link\n")?,
-            libc::S_IFBLK => out.write_all(b"block special file\n")?,
-            libc::S_IFCHR => out.write_all(b"character special file\n")?,
-            _ => out.write_all(b"unknown\n")?,
-        };
-        out.write_all(format!("Uid: {}\n", item.entry.uid).as_bytes())?;
-        out.write_all(format!("Gid: {}\n", item.entry.gid).as_bytes())?;
-        out.flush()?;
-        Ok(())
-    })
+/// This is expensive because the data has to be read from the pxar archive, which means reading
+/// over the network.
+async fn stat_command(path: String) -> Result<(), Error> {
+    Shell::with(move |shell| shell.stat(PathBuf::from(path))).await
 }
 
 #[api(
@@ -345,18 +182,8 @@ fn stat_command(path: String) -> Result<(), Error> {
 ///
 /// This will return an error if the entry is already present in the list or
 /// if an invalid path was provided.
-fn select_command(path: String) -> Result<(), Error> {
-    Context::with(|ctx| {
-        // Calling canonical_path() makes sure the provided path is valid and
-        // actually contained within the catalog and therefore also the archive.
-        let path = ctx.canonical_path(&path)?;
-        let pattern = MatchPattern::from_line(Context::generate_cstring(&path)?.as_bytes())?
-            .ok_or_else(|| format_err!("encountered invalid match pattern"))?;
-        if ctx.selected.iter().find(|p| **p == pattern).is_none() {
-            ctx.selected.push(pattern);
-        }
-        Ok(())
-    })
+async fn select_command(path: String) -> Result<(), Error> {
+    Shell::with(move |shell| shell.select(PathBuf::from(path))).await
 }
 
 #[api(
@@ -373,21 +200,52 @@ fn select_command(path: String) -> Result<(), Error> {
 ///
 /// This will return an error if the entry was not found in the list of entries
 /// selected for restore.
-fn deselect_command(path: String) -> Result<(), Error> {
-    Context::with(|ctx| {
-        let path = ctx.canonical_path(&path)?;
-        let mut pattern = MatchPattern::from_line(Context::generate_cstring(&path)?.as_bytes())?
-            .ok_or_else(|| format_err!("encountered invalid match pattern"))?;
-        if let Some(last) = ctx.selected.last() {
-            if last == &pattern {
-                ctx.selected.pop();
-                return Ok(());
-            }
-        }
-        pattern.invert();
-        ctx.selected.push(pattern);
-        Ok(())
-    })
+async fn deselect_command(path: String) -> Result<(), Error> {
+    Shell::with(move |shell| shell.deselect(PathBuf::from(path))).await
+}
+
+#[api( input: { properties: { } })]
+/// Clear the list of files selected for restore.
+async fn clear_selected_command() -> Result<(), Error> {
+    Shell::with(move |shell| shell.deselect_all()).await
+}
+
+#[api(
+    input: {
+        properties: {
+            patterns: {
+                type: Boolean,
+                description: "List match patterns instead of the matching files.",
+                optional: true,
+                default: false,
+            }
+        }
+    }
+)]
+/// List entries currently selected for restore.
+async fn list_selected_command(patterns: bool) -> Result<(), Error> {
+    Shell::with(move |shell| shell.list_selected(patterns)).await
+}
+
+#[api(
+    input: {
+        properties: {
+            pattern: {
+                type: String,
+                description: "Match pattern for matching files in the catalog."
+            },
+            select: {
+                type: bool,
+                optional: true,
+                default: false,
+                description: "Add matching filenames to list for restore."
+            }
+        }
+    }
+)]
+/// Find entries in the catalog matching the given match pattern.
+async fn find_command(pattern: String, select: bool) -> Result<(), Error> {
+    Shell::with(move |shell| shell.find(pattern, select)).await
 }
 
 #[api(
@@ -403,30 +261,8 @@ fn deselect_command(path: String) -> Result<(), Error> {
 /// Restore the selected entries to the given target path.
 ///
 /// Target must not exist on the clients filesystem.
-fn restore_selected_command(target: String) -> Result<(), Error> {
-    Context::with(|ctx| {
-        if ctx.selected.is_empty() {
-            bail!("no entries selected for restore");
-        }
-
-        // Entry point for the restore is always root here as the provided match
-        // patterns are relative to root as well.
-        let start_dir = ctx.decoder.root()?;
-        ctx.decoder
-            .restore(&start_dir, &Path::new(&target), &ctx.selected)?;
-        Ok(())
-    })
-}
-
-#[api( input: { properties: {} })]
-/// List entries currently selected for restore.
-fn list_selected_command() -> Result<(), Error> {
-    Context::with(|ctx| {
-        let mut out = std::io::stdout();
-        out.write_all(&MatchPattern::to_bytes(ctx.selected.as_slice()))?;
-        out.flush()?;
-        Ok(())
-    })
+async fn restore_selected_command(target: String) -> Result<(), Error> {
+    Shell::with(move |shell| shell.restore_selected(PathBuf::from(target))).await
 }
 
 #[api(
@@ -449,259 +285,1082 @@ fn list_selected_command() -> Result<(), Error> {
 /// By further providing a pattern, the restore can be limited to a narrower
 /// subset of this sub-archive.
 /// If pattern is not present or empty, the full archive is restored to target.
-fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error> {
-    Context::with(|ctx| {
-        let pattern = pattern.unwrap_or_default();
-        let match_pattern = match pattern.as_str() {
-            "" | "/" | "." => Vec::new(),
-            _ => vec![MatchPattern::from_line(pattern.as_bytes())?.unwrap()],
+async fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error> {
+    Shell::with(move |shell| shell.restore(PathBuf::from(target), pattern)).await
+}
+
+/// FIXME: Should we use this to fix `step()`?
+///
+/// The `Path` type's component iterator does not tell us anything about trailing slashes or
+/// trailing `Component::CurDir` entries. Since we only support regular paths we'll roll our own
+/// here:
+enum PathComponent<'a> {
+    Root,
+    CurDir,
+    ParentDir,
+    Normal(&'a OsStr),
+    TrailingSlash,
+}
+
+struct PathComponentIter<'a> {
+    path: &'a [u8],
+    state: u8, // 0=beginning, 1=ongoing, 2=trailing, 3=finished (fused)
+}
+
+impl std::iter::FusedIterator for PathComponentIter<'_> {}
+
+impl<'a> Iterator for PathComponentIter<'a> {
+    type Item = PathComponent<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.path.is_empty() {
+            return None;
+        }
+
+        if self.state == 0 {
+            self.state = 1;
+            if self.path[0] == b'/' {
+                // absolute path
+                self.path = &self.path[1..];
+                return Some(PathComponent::Root);
+            }
+        }
+
+        // skip slashes
+        let had_slashes = self.path[0] == b'/';
+        while self.path.get(0).copied() == Some(b'/') {
+            self.path = &self.path[1..];
+        }
+
+        Some(match self.path {
+            [] if had_slashes => PathComponent::TrailingSlash,
+            [] => return None,
+            [b'.'] | [b'.', b'/', ..] => {
+                self.path = &self.path[1..];
+                PathComponent::CurDir
+            }
+            [b'.', b'.'] | [b'.', b'.', b'/', ..] => {
+                self.path = &self.path[2..];
+                PathComponent::ParentDir
+            }
+            _ => {
+                let end = self
+                    .path
+                    .iter()
+                    .position(|&b| b == b'/')
+                    .unwrap_or(self.path.len());
+                let (out, rest) = self.path.split_at(end);
+                self.path = rest;
+                PathComponent::Normal(OsStr::from_bytes(out))
+            }
+        })
+    }
+}
+
+pub struct Shell {
+    /// Readline instance handling input and callbacks
+    rl: rustyline::Editor<CliHelper>,
+
+    /// Interactive prompt.
+    prompt: String,
+
+    /// Calalog reader instance to navigate
+    catalog: CatalogReader,
+
+    /// List of selected paths for restore
+    selected: HashMap<OsString, MatchEntry>,
+
+    /// pxar accessor instance for the current pxar archive
+    accessor: Accessor,
+
+    /// The current position in the archive.
+    position: Vec<PathStackEntry>,
+}
+
+#[derive(Clone)]
+struct PathStackEntry {
+    /// This is always available. We mainly navigate through the catalog.
+    catalog: catalog::DirEntry,
+
+    /// Whenever we need something from the actual archive we fill this out. This is cached along
+    /// the entire path.
+    pxar: Option<FileEntry>,
+}
+
+impl PathStackEntry {
+    fn new(dir_entry: catalog::DirEntry) -> Self {
+        Self {
+            pxar: None,
+            catalog: dir_entry,
+        }
+    }
+}
+
+impl Shell {
+    /// Create a new shell for the given catalog and pxar archive.
+    pub async fn new(
+        mut catalog: CatalogReader,
+        archive_name: &str,
+        archive: Accessor,
+    ) -> Result<Self, Error> {
+        let cli_helper = CliHelper::new(catalog_shell_cli());
+        let mut rl = rustyline::Editor::<CliHelper>::new();
+        rl.set_helper(Some(cli_helper));
+
+        let catalog_root = catalog.root()?;
+        let archive_root = catalog
+            .lookup(&catalog_root, archive_name.as_bytes())?
+            .ok_or_else(|| format_err!("archive not found in catalog"))?;
+        let position = vec![PathStackEntry::new(archive_root)];
+
+        let mut this = Self {
+            rl,
+            prompt: String::new(),
+            catalog,
+            selected: HashMap::new(),
+            accessor: archive,
+            position,
         };
-        // Decoder entry point for the restore.
-        let start_dir = if pattern.starts_with("/") {
-            ctx.decoder.root()?
+        this.update_prompt();
+        Ok(this)
+    }
+
+    async fn with<'a, Fut, R, F>(call: F) -> Result<R, Error>
+    where
+        F: FnOnce(&'a mut Shell) -> Fut,
+        Fut: Future<Output = Result<R, Error>>,
+        F: 'a,
+        Fut: 'a,
+        R: 'static,
+    {
+        let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) };
+        let result = call(&mut *shell).await;
+        result
+    }
+
+    pub async fn shell(mut self) -> Result<(), Error> {
+        let this = &mut self;
+        unsafe {
+            SHELL = Some(this as *mut Shell as usize);
+        }
+        while let Ok(line) = this.rl.readline(&this.prompt) {
+            let helper = this.rl.helper().unwrap();
+            let args = match cli::shellword_split(&line) {
+                Ok(args) => args,
+                Err(err) => {
+                    println!("Error: {}", err);
+                    continue;
+                }
+            };
+
+            let _ =
+                cli::handle_command_future(helper.cmd_def(), "", args, cli::CliEnvironment::new())
+                    .await;
+            this.rl.add_history_entry(line);
+            this.update_prompt();
+        }
+        Ok(())
+    }
+
+    fn update_prompt(&mut self) {
+        self.prompt = "pxar:".to_string();
+        if self.position.len() <= 1 {
+            self.prompt.push('/');
         } else {
-            // Get the directory corresponding to the working directory from the
-            // archive.
-            let cwd = ctx.current.clone();
-            let (dir, _, _) = ctx.lookup(&cwd)?;
-            dir
+            for p in self.position.iter().skip(1) {
+                if !p.catalog.name.starts_with(b"/") {
+                    self.prompt.push('/');
+                }
+                match std::str::from_utf8(&p.catalog.name) {
+                    Ok(entry) => self.prompt.push_str(entry),
+                    Err(_) => self.prompt.push_str("<non-utf8-dir>"),
+                }
+            }
+        }
+        self.prompt.push_str(" > ");
+    }
+
+    async fn pwd(&mut self) -> Result<(), Error> {
+        let stack = Self::lookup(
+            &self.position,
+            &mut self.catalog,
+            &self.accessor,
+            None,
+            &mut Some(0),
+        )
+        .await?;
+        let path = Self::format_path_stack(&stack);
+        println!("{:?}", path);
+        Ok(())
+    }
+
+    fn new_path_stack(&self) -> Vec<PathStackEntry> {
+        self.position[..1].to_vec()
+    }
+
+    async fn resolve_symlink(
+        stack: &mut Vec<PathStackEntry>,
+        catalog: &mut CatalogReader,
+        accessor: &Accessor,
+        follow_symlinks: &mut Option<usize>,
+    ) -> Result<(), Error> {
+        if let Some(ref mut symlink_count) = follow_symlinks {
+            *symlink_count += 1;
+            if *symlink_count > MAX_SYMLINK_COUNT {
+                bail!("too many levels of symbolic links");
+            }
+
+            let file = Self::walk_pxar_archive(accessor, &mut stack[..]).await?;
+
+            let path = match file.entry().kind() {
+                EntryKind::Symlink(symlink) => Path::new(symlink.as_os_str()),
+                _ => bail!("symlink in the catalog was not a symlink in the archive"),
+            };
+
+            let new_stack =
+                Self::lookup(&stack, &mut *catalog, accessor, Some(path), follow_symlinks).await?;
+
+            *stack = new_stack;
+
+            Ok(())
+        } else {
+            bail!("target is a symlink");
+        }
+    }
+
+    /// Walk a path and add it to the path stack.
+    ///
+    /// If the symlink count is used, symlinks will be followed, until we hit the cap and error
+    /// out.
+    async fn step(
+        stack: &mut Vec<PathStackEntry>,
+        catalog: &mut CatalogReader,
+        accessor: &Accessor,
+        component: std::path::Component<'_>,
+        follow_symlinks: &mut Option<usize>,
+    ) -> Result<(), Error> {
+        use std::path::Component;
+        match component {
+            Component::Prefix(_) => bail!("invalid path component (prefix)"),
+            Component::RootDir => stack.truncate(1),
+            Component::CurDir => {
+                if stack.last().unwrap().catalog.is_symlink() {
+                    Self::resolve_symlink(stack, catalog, accessor, follow_symlinks).await?;
+                }
+            }
+            Component::ParentDir => drop(stack.pop()),
+            Component::Normal(entry) => {
+                if stack.last().unwrap().catalog.is_symlink() {
+                    Self::resolve_symlink(stack, catalog, accessor, follow_symlinks).await?;
+                }
+                match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? {
+                    Some(dir) => stack.push(PathStackEntry::new(dir)),
+                    None => bail!("no such file or directory: {:?}", entry),
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn step_nofollow(
+        stack: &mut Vec<PathStackEntry>,
+        catalog: &mut CatalogReader,
+        component: std::path::Component<'_>,
+    ) -> Result<(), Error> {
+        use std::path::Component;
+        match component {
+            Component::Prefix(_) => bail!("invalid path component (prefix)"),
+            Component::RootDir => stack.truncate(1),
+            Component::CurDir => {
+                if stack.last().unwrap().catalog.is_symlink() {
+                    bail!("target is a symlink");
+                }
+            }
+            Component::ParentDir => drop(stack.pop()),
+            Component::Normal(entry) => {
+                if stack.last().unwrap().catalog.is_symlink() {
+                    bail!("target is a symlink");
+                } else {
+                    match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? {
+                        Some(dir) => stack.push(PathStackEntry::new(dir)),
+                        None => bail!("no such file or directory: {:?}", entry),
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
+
+    /// The pxar accessor is required to resolve symbolic links
+    async fn walk_catalog(
+        stack: &mut Vec<PathStackEntry>,
+        catalog: &mut CatalogReader,
+        accessor: &Accessor,
+        path: &Path,
+        follow_symlinks: &mut Option<usize>,
+    ) -> Result<(), Error> {
+        for c in path.components() {
+            Self::step(stack, catalog, accessor, c, follow_symlinks).await?;
+        }
+        Ok(())
+    }
+
+    /// Non-async version cannot follow symlinks.
+    fn walk_catalog_nofollow(
+        stack: &mut Vec<PathStackEntry>,
+        catalog: &mut CatalogReader,
+        path: &Path,
+    ) -> Result<(), Error> {
+        for c in path.components() {
+            Self::step_nofollow(stack, catalog, c)?;
+        }
+        Ok(())
+    }
+
+    /// This assumes that there are no more symlinks in the path stack.
+    async fn walk_pxar_archive(
+        accessor: &Accessor,
+        mut stack: &mut [PathStackEntry],
+    ) -> Result<FileEntry, Error> {
+        if stack[0].pxar.is_none() {
+            stack[0].pxar = Some(accessor.open_root().await?.lookup_self().await?);
+        }
+
+        // Now walk the directory stack:
+        let mut at = 1;
+        while at < stack.len() {
+            if stack[at].pxar.is_some() {
+                at += 1;
+                continue;
+            }
+
+            let parent = stack[at - 1].pxar.as_ref().unwrap();
+            let dir = parent.enter_directory().await?;
+            let name = Path::new(OsStr::from_bytes(&stack[at].catalog.name));
+            stack[at].pxar = Some(
+                dir.lookup(name)
+                    .await?
+                    .ok_or_else(|| format_err!("no such entry in pxar file: {:?}", name))?,
+            );
+
+            at += 1;
+        }
+
+        Ok(stack.last().unwrap().pxar.clone().unwrap())
+    }
+
+    fn complete_path(&mut self, input: &str) -> Result<Vec<String>, Error> {
+        let mut tmp_stack;
+        let (parent, base, part) = match input.rfind('/') {
+            Some(ind) => {
+                let (base, part) = input.split_at(ind + 1);
+                let path = PathBuf::from(base);
+                if path.is_absolute() {
+                    tmp_stack = self.new_path_stack();
+                } else {
+                    tmp_stack = self.position.clone();
+                }
+                Self::walk_catalog_nofollow(&mut tmp_stack, &mut self.catalog, &path)?;
+                (&tmp_stack.last().unwrap().catalog, base, part)
+            }
+            None => (&self.position.last().unwrap().catalog, "", input),
         };
 
-        ctx.decoder
-            .restore(&start_dir, &Path::new(&target), &match_pattern)?;
+        let entries = self.catalog.read_dir(parent)?;
+
+        let mut out = Vec::new();
+        for entry in entries {
+            let mut name = base.to_string();
+            if entry.name.starts_with(part.as_bytes()) {
+                name.push_str(std::str::from_utf8(&entry.name)?);
+                if entry.is_directory() {
+                    name.push('/');
+                }
+                out.push(name);
+            }
+        }
+
+        Ok(out)
+    }
+
+    // Break async recursion here: lookup -> walk_catalog -> step -> lookup
+    fn lookup<'future, 's, 'c, 'a, 'p, 'y>(
+        stack: &'s [PathStackEntry],
+        catalog: &'c mut CatalogReader,
+        accessor: &'a Accessor,
+        path: Option<&'p Path>,
+        follow_symlinks: &'y mut Option<usize>,
+    ) -> Pin<Box<dyn Future<Output = Result<Vec<PathStackEntry>, Error>> + Send + 'future>>
+    where
+        's: 'future,
+        'c: 'future,
+        'a: 'future,
+        'p: 'future,
+        'y: 'future,
+    {
+        Box::pin(async move {
+            Ok(match path {
+                None => stack.to_vec(),
+                Some(path) => {
+                    let mut stack = if path.is_absolute() {
+                        stack[..1].to_vec()
+                    } else {
+                        stack.to_vec()
+                    };
+                    Self::walk_catalog(&mut stack, catalog, accessor, path, follow_symlinks)
+                        .await?;
+                    stack
+                }
+            })
+        })
+    }
+
+    async fn ls(&mut self, path: Option<&Path>) -> Result<(), Error> {
+        let stack = Self::lookup(
+            &self.position,
+            &mut self.catalog,
+            &self.accessor,
+            path,
+            &mut Some(0),
+        )
+        .await?;
+
+        let last = stack.last().unwrap();
+        if last.catalog.is_directory() {
+            let items = self.catalog.read_dir(&stack.last().unwrap().catalog)?;
+            let mut out = std::io::stdout();
+            // FIXME: columnize
+            for item in items {
+                out.write_all(&item.name)?;
+                out.write_all(b"\n")?;
+            }
+        } else {
+            let mut out = std::io::stdout();
+            out.write_all(&last.catalog.name)?;
+            out.write_all(b"\n")?;
+        }
         Ok(())
-    })
-}
+    }
 
-#[api(
-    input: {
-        properties: {
-            path: {
-                type: String,
-                description: "Path to node from where to start the search."
-            },
-            pattern: {
-                type: String,
-                description: "Match pattern for matching files in the catalog."
-            },
-            select: {
-                type: bool,
-                optional: true,
-                description: "Add matching filenames to list for restore."
+    async fn stat(&mut self, path: PathBuf) -> Result<(), Error> {
+        let mut stack = Self::lookup(
+            &self.position,
+            &mut self.catalog,
+            &self.accessor,
+            Some(&path),
+            &mut Some(0),
+        )
+        .await?;
+
+        let file = Self::walk_pxar_archive(&self.accessor, &mut stack).await?;
+        std::io::stdout()
+            .write_all(crate::pxar::format_multi_line_entry(file.entry()).as_bytes())?;
+        Ok(())
+    }
+
+    async fn cd(&mut self, path: Option<&Path>) -> Result<(), Error> {
+        match path {
+            Some(path) => {
+                let new_position = Self::lookup(
+                    &self.position,
+                    &mut self.catalog,
+                    &self.accessor,
+                    Some(path),
+                    &mut None,
+                )
+                .await?;
+                if !new_position.last().unwrap().catalog.is_directory() {
+                    bail!("not a directory");
+                }
+                self.position = new_position;
             }
+            None => self.position.truncate(1),
         }
+        self.update_prompt();
+        Ok(())
     }
-)]
-/// Find entries in the catalog matching the given match pattern.
-fn find_command(path: String, pattern: String, select: Option<bool>) -> Result<(), Error> {
-    Context::with(|ctx| {
-        let path = ctx.canonical_path(&path)?;
-        if !path.last().unwrap().is_directory() {
-            bail!("path should be a directory, not a file!");
-        }
-        let select = select.unwrap_or(false);
-
-        let cpath = Context::generate_cstring(&path).unwrap();
-        let pattern = if pattern.starts_with("!") {
-            let mut buffer = vec![b'!'];
-            buffer.extend_from_slice(cpath.as_bytes());
-            buffer.extend_from_slice(pattern[1..pattern.len()].as_bytes());
-            buffer
+
+    /// This stack must have been canonicalized already!
+    fn format_path_stack(stack: &[PathStackEntry]) -> OsString {
+        if stack.len() <= 1 {
+            return OsString::from("/");
+        }
+
+        let mut out = OsString::new();
+        for c in stack.iter().skip(1) {
+            out.push("/");
+            out.push(OsStr::from_bytes(&c.catalog.name));
+        }
+
+        out
+    }
+
+    async fn select(&mut self, path: PathBuf) -> Result<(), Error> {
+        let stack = Self::lookup(
+            &self.position,
+            &mut self.catalog,
+            &self.accessor,
+            Some(&path),
+            &mut Some(0),
+        )
+        .await?;
+
+        let path = Self::format_path_stack(&stack);
+        let entry = MatchEntry::include(MatchPattern::Literal(path.as_bytes().to_vec()));
+        if self.selected.insert(path.clone(), entry).is_some() {
+            println!("path already selected: {:?}", path);
         } else {
-            let mut buffer = cpath.as_bytes().to_vec();
-            buffer.extend_from_slice(pattern.as_bytes());
-            buffer
-        };
+            println!("added path: {:?}", path);
+        }
+
+        Ok(())
+    }
+
+    async fn deselect(&mut self, path: PathBuf) -> Result<(), Error> {
+        let stack = Self::lookup(
+            &self.position,
+            &mut self.catalog,
+            &self.accessor,
+            Some(&path),
+            &mut Some(0),
+        )
+        .await?;
+
+        let path = Self::format_path_stack(&stack);
+
+        if self.selected.remove(&path).is_some() {
+            println!("removed path from selection: {:?}", path);
+        } else {
+            println!("path not selected: {:?}", path);
+        }
+
+        Ok(())
+    }
+
+    async fn deselect_all(&mut self) -> Result<(), Error> {
+        self.selected.clear();
+        println!("cleared selection");
+        Ok(())
+    }
 
-        let pattern = MatchPattern::from_line(&pattern)?
-            .ok_or_else(|| format_err!("invalid match pattern"))?;
-        let slice = vec![pattern.as_slice()];
-
-        // The match pattern all contain the prefix of the entry path in order to
-        // store them if selected, so the entry point for find is always the root
-        // directory.
-        let mut dir_stack = ctx.root.clone();
-        ctx.catalog.find(
-            &mut dir_stack,
-            &slice,
-            &Box::new(|path: &[DirEntry]| println!("{:?}", Context::generate_cstring(path).unwrap()))
+    async fn list_selected(&mut self, patterns: bool) -> Result<(), Error> {
+        if patterns {
+            self.list_selected_patterns().await
+        } else {
+            self.list_matching_files().await
+        }
+    }
+
+    async fn list_selected_patterns(&self) -> Result<(), Error> {
+        for entry in self.selected.keys() {
+            println!("{:?}", entry);
+        }
+        Ok(())
+    }
+
+    fn build_match_list(&self) -> Vec<MatchEntry> {
+        let mut list = Vec::with_capacity(self.selected.len());
+        for entry in self.selected.values() {
+            list.push(entry.clone());
+        }
+        list
+    }
+
+    async fn list_matching_files(&mut self) -> Result<(), Error> {
+        let matches = self.build_match_list();
+
+        self.catalog.find(
+            &self.position[0].catalog,
+            &mut Vec::new(),
+            &matches,
+            &mut |path: &[u8]| -> Result<(), Error> {
+                let mut out = std::io::stdout();
+                out.write_all(path)?;
+                out.write_all(b"\n")?;
+                Ok(())
+            },
+        )?;
+
+        Ok(())
+    }
+
+    async fn find(&mut self, pattern: String, select: bool) -> Result<(), Error> {
+        let pattern_os = OsString::from(pattern.clone());
+        let pattern_entry =
+            MatchEntry::parse_pattern(pattern, PatternFlag::PATH_NAME, MatchType::Include)?;
+
+        let mut found_some = false;
+        self.catalog.find(
+            &self.position[0].catalog,
+            &mut Vec::new(),
+            &[&pattern_entry],
+            &mut |path: &[u8]| -> Result<(), Error> {
+                found_some = true;
+                let mut out = std::io::stdout();
+                out.write_all(path)?;
+                out.write_all(b"\n")?;
+                Ok(())
+            },
         )?;
 
-        // Insert if matches should be selected.
-        // Avoid duplicate entries of the same match pattern.
-        if select && ctx.selected.iter().find(|p| **p == pattern).is_none() {
-            ctx.selected.push(pattern);
+        if found_some && select {
+            self.selected.insert(pattern_os, pattern_entry);
         }
 
         Ok(())
-    })
+    }
+
+    async fn restore_selected(&mut self, destination: PathBuf) -> Result<(), Error> {
+        if self.selected.is_empty() {
+            bail!("no entries selected");
+        }
+
+        let match_list = self.build_match_list();
+
+        self.restore_with_match_list(destination, &match_list).await
+    }
+
+    async fn restore(
+        &mut self,
+        destination: PathBuf,
+        pattern: Option<String>,
+    ) -> Result<(), Error> {
+        let tmp;
+        let match_list: &[MatchEntry] = match pattern {
+            None => &[],
+            Some(pattern) => {
+                tmp = [MatchEntry::parse_pattern(
+                    pattern,
+                    PatternFlag::PATH_NAME,
+                    MatchType::Include,
+                )?];
+                &tmp
+            }
+        };
+
+        self.restore_with_match_list(destination, match_list).await
+    }
+
+    async fn restore_with_match_list(
+        &mut self,
+        destination: PathBuf,
+        match_list: &[MatchEntry],
+    ) -> Result<(), Error> {
+        create_path(
+            &destination,
+            None,
+            Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
+        )
+        .map_err(|err| format_err!("error creating directory {:?}: {}", destination, err))?;
+
+        let rootdir = Dir::open(
+            &destination,
+            OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
+            Mode::empty(),
+        )
+        .map_err(|err| {
+            format_err!("unable to open target directory {:?}: {}", destination, err,)
+        })?;
+
+        let mut dir_stack = self.new_path_stack();
+        Self::walk_pxar_archive(&self.accessor, &mut dir_stack).await?;
+        let root_meta = dir_stack
+            .last()
+            .unwrap()
+            .pxar
+            .as_ref()
+            .unwrap()
+            .entry()
+            .metadata()
+            .clone();
+        let pxar_dir_stack = PxarDirStack::new(rootdir, root_meta);
+
+        let mut extractor = ExtractorState::new(
+            Flags::DEFAULT,
+            &mut self.catalog,
+            dir_stack,
+            pxar_dir_stack,
+            &match_list,
+            &self.accessor,
+        )?;
+
+        extractor.extract().await
+    }
 }
 
-std::thread_local! {
-    static CONTEXT: RefCell<Option<Context>> = RefCell::new(None);
+enum LoopState {
+    Break,
+    Continue,
 }
 
-/// Holds the context needed for access to catalog and decoder
-struct Context {
-    /// Calalog reader instance to navigate
-    catalog: CatalogReader<std::fs::File>,
-    /// List of selected paths for restore
-    selected: Vec<MatchPattern>,
-    /// Decoder instance for the current pxar archive
-    decoder: Decoder,
-    /// Root directory for the give archive as stored in the catalog
-    root: Vec<DirEntry>,
-    /// Stack of directories up to the current working directory
-    /// used for navigation and path completion.
-    current: Vec<DirEntry>,
+struct ExtractorState<'a> {
+    path: Vec<u8>,
+    path_len: usize,
+    path_len_stack: Vec<usize>,
+
+    dir_stack: Vec<PathStackEntry>,
+
+    matches: bool,
+    matches_stack: Vec<bool>,
+
+    read_dir: <Vec<catalog::DirEntry> as IntoIterator>::IntoIter,
+    read_dir_stack: Vec<<Vec<catalog::DirEntry> as IntoIterator>::IntoIter>,
+
+    pxar_dir_stack: PxarDirStack,
+
+    catalog: &'a mut CatalogReader,
+    feature_flags: Flags,
+    match_list: &'a [MatchEntry],
+    accessor: &'a Accessor,
 }
 
-impl Context {
-    /// Execute `call` within a context providing a mut ref to `Context` instance.
-    fn with<T, F>(call: F) -> Result<T, Error>
-    where
-        F: FnOnce(&mut Context) -> Result<T, Error>,
-    {
-        CONTEXT.with(|cell| {
-            let mut ctx = cell.borrow_mut();
-            call(&mut ctx.as_mut().unwrap())
+impl<'a> ExtractorState<'a> {
+    pub fn new(
+        feature_flags: Flags,
+        catalog: &'a mut CatalogReader,
+        dir_stack: Vec<PathStackEntry>,
+        pxar_dir_stack: PxarDirStack,
+        match_list: &'a [MatchEntry],
+        accessor: &'a Accessor,
+    ) -> Result<Self, Error> {
+        let read_dir = catalog
+            .read_dir(&dir_stack.last().unwrap().catalog)?
+            .into_iter();
+        Ok(Self {
+            path: Vec::new(),
+            path_len: 0,
+            path_len_stack: Vec::new(),
+
+            dir_stack,
+
+            matches: match_list.is_empty(),
+            matches_stack: Vec::new(),
+
+            read_dir,
+            read_dir_stack: Vec::new(),
+
+            pxar_dir_stack,
+
+            catalog,
+            feature_flags,
+            match_list,
+            accessor,
         })
     }
 
-    /// Generate CString from provided stack of `DirEntry`s.
-    fn generate_cstring(dir_stack: &[DirEntry]) -> Result<CString, Error> {
-        let mut path = vec![b'/'];
-        // Skip the archive root, the '/' is displayed for it instead
-        for component in dir_stack.iter().skip(1) {
-            path.extend_from_slice(&component.name);
-            if component.is_directory() {
-                path.push(b'/');
+    pub async fn extract(&mut self) -> Result<(), Error> {
+        loop {
+            let entry = match self.read_dir.next() {
+                Some(entry) => entry,
+                None => match self.handle_end_of_directory()? {
+                    LoopState::Break => break, // done with root directory
+                    LoopState::Continue => continue,
+                },
+            };
+
+            self.path.truncate(self.path_len);
+            if !entry.name.starts_with(b"/") {
+                self.path.reserve(entry.name.len() + 1);
+                self.path.push(b'/');
             }
+            self.path.extend(&entry.name);
+
+            self.handle_entry(entry).await?;
         }
-        Ok(unsafe { CString::from_vec_unchecked(path) })
+
+        Ok(())
     }
 
-    /// Resolve the indirect path components and return an absolute path.
-    ///
-    /// This will actually navigate the filesystem tree to check that the
-    /// path is vaild and exists.
-    /// This does not include following symbolic links.
-    /// If None is given as path, only the root directory is returned.
-    fn canonical_path(&mut self, path: &str) -> Result<Vec<DirEntry>, Error> {
-        if path == "/" {
-            return Ok(self.root.clone());
-        }
-
-        let mut path_slice = if path.is_empty() {
-            // Fallback to root if no path was provided
-            return Ok(self.root.clone());
-        } else {
-            path
+    fn handle_end_of_directory(&mut self) -> Result<LoopState, Error> {
+        // go up a directory:
+        self.read_dir = match self.read_dir_stack.pop() {
+            Some(r) => r,
+            None => return Ok(LoopState::Break), // out of root directory
         };
 
-        let mut dir_stack = if path_slice.starts_with("/") {
-            // Absolute path, reduce view of slice and start from root
-            path_slice = &path_slice[1..];
-            self.root.clone()
-        } else {
-            // Relative path, start from current working directory
-            self.current.clone()
-        };
-        let should_end_dir = if path_slice.ends_with("/") {
-            path_slice = &path_slice[0..path_slice.len() - 1];
-            true
-        } else {
-            false
+        self.matches = self
+            .matches_stack
+            .pop()
+            .ok_or_else(|| format_err!("internal iterator error (matches_stack)"))?;
+
+        self.dir_stack
+            .pop()
+            .ok_or_else(|| format_err!("internal iterator error (dir_stack)"))?;
+
+        let dir = self
+            .pxar_dir_stack
+            .pop()?
+            .ok_or_else(|| format_err!("internal iterator error (pxar_dir_stack)"))?;
+
+        self.path_len = self
+            .path_len_stack
+            .pop()
+            .ok_or_else(|| format_err!("internal iterator error (path_len_stack)"))?;
+
+        self.path.push(0);
+        let dirname = CStr::from_bytes_with_nul(&self.path[(self.path_len + 1)..])?;
+
+        if let Some(fd) = dir.try_as_raw_fd() {
+            // the directory was created, so apply the metadata:
+            metadata::apply(self.feature_flags, dir.metadata(), fd, dirname)?;
+        }
+
+        Ok(LoopState::Continue)
+    }
+
+    async fn handle_new_directory(
+        &mut self,
+        entry: catalog::DirEntry,
+        match_result: Option<MatchType>,
+    ) -> Result<(), Error> {
+        // enter a new directory:
+        self.read_dir_stack.push(mem::replace(
+            &mut self.read_dir,
+            self.catalog.read_dir(&entry)?.into_iter(),
+        ));
+        self.matches_stack.push(self.matches);
+        self.dir_stack.push(PathStackEntry::new(entry));
+        self.path_len_stack.push(self.path_len);
+        self.path_len = self.path.len();
+
+        Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?;
+        let dir_pxar = self.dir_stack.last().unwrap().pxar.as_ref().unwrap();
+        let dir_meta = dir_pxar.entry().metadata().clone();
+        self.pxar_dir_stack
+            .push(dir_pxar.file_name().to_os_string(), dir_meta)?;
+
+        if self.matches && match_result != Some(MatchType::Exclude) {
+            todo!("create this directory");
+        }
+
+        Ok(())
+    }
+
+    pub async fn handle_entry(&mut self, entry: catalog::DirEntry) -> Result<(), Error> {
+        let match_result = self.match_list.matches(&self.path, entry.get_file_mode());
+        let did_match = match match_result {
+            Some(MatchType::Include) => true,
+            Some(MatchType::Exclude) => false,
+            None => self.matches,
         };
-        for name in path_slice.split('/') {
-            match name {
-                "." => continue,
-                ".." => {
-                    // Never pop archive root from stack
-                    if dir_stack.len() > 1 {
-                        dir_stack.pop();
-                    }
-                }
-                _ => {
-                    let entry = self.catalog.lookup(dir_stack.last().unwrap(), name.as_bytes())?;
-                    dir_stack.push(entry);
-                }
+
+        match (did_match, &entry.attr) {
+            (_, DirEntryAttribute::Directory { .. }) => {
+                self.handle_new_directory(entry, match_result).await?;
             }
+            (true, DirEntryAttribute::File { .. }) => {
+                self.dir_stack.push(PathStackEntry::new(entry));
+                let file = Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?;
+                self.extract_file(file).await?;
+                self.dir_stack.pop();
+            }
+            (true, DirEntryAttribute::Symlink)
+            | (true, DirEntryAttribute::BlockDevice)
+            | (true, DirEntryAttribute::CharDevice)
+            | (true, DirEntryAttribute::Fifo)
+            | (true, DirEntryAttribute::Socket)
+            | (true, DirEntryAttribute::Hardlink) => {
+                let attr = entry.attr.clone();
+                self.dir_stack.push(PathStackEntry::new(entry));
+                let file = Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?;
+                self.extract_special(file, attr).await?;
+                self.dir_stack.pop();
+            }
+            (false, _) => (), // skip
         }
-        if should_end_dir
-            && !dir_stack
-                .last()
-                .ok_or_else(|| format_err!("invalid path component"))?
-                .is_directory()
-        {
-            bail!("entry is not a directory");
+
+        Ok(())
+    }
+
+    fn path(&self) -> &OsStr {
+        OsStr::from_bytes(&self.path)
+    }
+
+    async fn extract_file(&mut self, entry: FileEntry) -> Result<(), Error> {
+        match entry.kind() {
+            pxar::EntryKind::File { size, .. } => {
+                let mut contents = entry.contents().await?;
+
+                let parent = self.pxar_dir_stack.last_dir_fd(true)?;
+                let mut file = tokio::fs::File::from_std(unsafe {
+                    std::fs::File::from_raw_fd(nix::fcntl::openat(
+                        parent,
+                        entry.file_name(),
+                        OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_CLOEXEC,
+                        Mode::from_bits(0o600).unwrap(),
+                    )?)
+                });
+
+                let extracted = tokio::io::copy(&mut contents, &mut file).await?;
+                if *size != extracted {
+                    bail!("extracted {} bytes of a file of {} bytes", extracted, size);
+                }
+
+                metadata::apply_with_path(
+                    Flags::DEFAULT,
+                    entry.metadata(),
+                    file.as_raw_fd(),
+                    entry.file_name(),
+                )?;
+
+                Ok(())
+            }
+            _ => {
+                bail!(
+                    "catalog file {:?} not a regular file in the archive",
+                    self.path()
+                );
+            }
         }
+    }
+
+    async fn extract_special(
+        &mut self,
+        entry: FileEntry,
+        catalog_attr: DirEntryAttribute,
+    ) -> Result<(), Error> {
+        match (catalog_attr, entry.kind()) {
+            (DirEntryAttribute::Symlink, pxar::EntryKind::Symlink(symlink)) => {
+                self.extract_symlink(entry.file_name(), symlink.as_os_str(), entry.metadata())
+            }
+            (DirEntryAttribute::Symlink, _) => {
+                bail!(
+                    "catalog symlink {:?} not a symlink in the archive",
+                    self.path()
+                );
+            }
+
+            (DirEntryAttribute::Hardlink, pxar::EntryKind::Hardlink(hardlink)) => {
+                self.extract_hardlink(entry.file_name(), hardlink.as_os_str(), entry.metadata())
+            }
+            (DirEntryAttribute::Hardlink, _) => {
+                bail!(
+                    "catalog hardlink {:?} not a hardlink in the archive",
+                    self.path()
+                );
+            }
+
+            (ref attr, pxar::EntryKind::Device(device)) => {
+                self.extract_device(attr.clone(), entry.file_name(), device, entry.metadata())
+            }
 
-        Ok(dir_stack)
+            (DirEntryAttribute::Fifo, pxar::EntryKind::Fifo) => {
+                self.extract_node(entry.file_name(), 0, entry.metadata())
+            }
+            (DirEntryAttribute::Fifo, _) => {
+                bail!("catalog fifo {:?} not a fifo in the archive", self.path());
+            }
+
+            (DirEntryAttribute::Socket, pxar::EntryKind::Socket) => {
+                self.extract_node(entry.file_name(), 0, entry.metadata())
+            }
+            (DirEntryAttribute::Socket, _) => {
+                bail!(
+                    "catalog socket {:?} not a socket in the archive",
+                    self.path()
+                );
+            }
+
+            attr => bail!("unhandled file type {:?} for {:?}", attr, self.path()),
+        }
     }
 
-    /// Generate the CString to display by readline based on
-    /// PROMPT_PREFIX, PROMPT and the current working directory.
-    fn generate_prompt(&self) -> Result<String, Error> {
-        let prompt = format!(
-            "{}{} {} ",
-            PROMPT_PREFIX,
-            Self::generate_cstring(&self.current)?.to_string_lossy(),
-            PROMPT,
-        );
-        Ok(prompt)
+    fn extract_symlink(
+        &mut self,
+        file_name: &OsStr,
+        target: &OsStr,
+        metadata: &Metadata,
+    ) -> Result<(), Error> {
+        let parent = self.pxar_dir_stack.last_dir_fd(true)?;
+        nix::unistd::symlinkat(target, Some(parent), file_name)?;
+
+        metadata::apply_at(
+            self.feature_flags,
+            metadata,
+            parent,
+            &CString::new(file_name.as_bytes())?,
+        )?;
+
+        Ok(())
     }
 
-    /// Get the current size of the terminal
-    /// # Safety
-    ///
-    /// uses unsafe call to tty_ioctl, see man tty_ioctl(2)
-    fn get_terminal_size() -> (usize, usize) {
-        const TIOCGWINSZ: libc::c_ulong = 0x5413;
-
-        #[repr(C)]
-        struct WinSize {
-            ws_row: libc::c_ushort,
-            ws_col: libc::c_ushort,
-            _ws_xpixel: libc::c_ushort, // unused
-            _ws_ypixel: libc::c_ushort, // unused
-        }
-
-        let mut winsize = WinSize {
-            ws_row: 0,
-            ws_col: 0,
-            _ws_xpixel: 0,
-            _ws_ypixel: 0,
-        };
-        unsafe { libc::ioctl(libc::STDOUT_FILENO, TIOCGWINSZ, &mut winsize) };
-        (winsize.ws_row as usize, winsize.ws_col as usize)
+    fn extract_hardlink(
+        &mut self,
+        file_name: &OsStr,
+        target: &OsStr,
+        _metadata: &Metadata,
+    ) -> Result<(), Error> {
+        crate::pxar::tools::assert_relative_path(target)?;
+
+        let parent = self.pxar_dir_stack.last_dir_fd(true)?;
+        let root = self.pxar_dir_stack.root_dir_fd()?;
+        nix::unistd::linkat(
+            Some(root),
+            target,
+            Some(parent),
+            file_name,
+            nix::unistd::LinkatFlags::NoSymlinkFollow,
+        )?;
+
+        Ok(())
     }
 
-    /// Look up the entry given by a canonical absolute `path` in the archive.
-    ///
-    /// This will actively navigate the archive by calling the corresponding
-    /// decoder functionalities and is therefore very expensive.
-    fn lookup(
+    fn extract_device(
         &mut self,
-        absolute_path: &[DirEntry],
-    ) -> Result<(DirectoryEntry, PxarAttributes, u64), Error> {
-        let mut current = self.decoder.root()?;
-        let (_, _, mut attr, mut size) = self.decoder.attributes(0)?;
-        // Ignore the archive root, don't need it.
-        for item in absolute_path.iter().skip(1) {
-            match self
-                .decoder
-                .lookup(&current, &OsStr::from_bytes(&item.name))?
-            {
-                Some((item, item_attr, item_size)) => {
-                    current = item;
-                    attr = item_attr;
-                    size = item_size;
+        attr: DirEntryAttribute,
+        file_name: &OsStr,
+        device: &pxar::format::Device,
+        metadata: &Metadata,
+    ) -> Result<(), Error> {
+        match attr {
+            DirEntryAttribute::BlockDevice => {
+                if !metadata.stat.is_blockdev() {
+                    bail!(
+                        "catalog block device {:?} is not a block device in the archive",
+                        self.path(),
+                    );
+                }
+            }
+            DirEntryAttribute::CharDevice => {
+                if !metadata.stat.is_chardev() {
+                    bail!(
+                        "catalog character device {:?} is not a character device in the archive",
+                        self.path(),
+                    );
                 }
-                // This should not happen if catalog an archive are consistent.
-                None => bail!("no such file or directory in archive - inconsistent catalog"),
+            }
+            _ => {
+                bail!(
+                    "unexpected file type for {:?} in the catalog, \
+                     which is a device special file in the archive",
+                    self.path(),
+                );
             }
         }
-        Ok((current, attr, size))
+        self.extract_node(file_name, device.to_dev_t(), metadata)
+    }
+
+    fn extract_node(
+        &mut self,
+        file_name: &OsStr,
+        device: libc::dev_t,
+        metadata: &Metadata,
+    ) -> Result<(), Error> {
+        let mode = metadata.stat.mode;
+        let mode = u32::try_from(mode).map_err(|_| {
+            format_err!(
+                "device node's mode contains illegal bits: 0x{:x} (0o{:o})",
+                mode,
+                mode,
+            )
+        })?;
+
+        let parent = self.pxar_dir_stack.last_dir_fd(true)?;
+        let file_name = CString::new(file_name.as_bytes())?;
+        unsafe { c_result!(libc::mknodat(parent, file_name.as_ptr(), mode, device)) }
+            .map_err(|err| format_err!("failed to create device node: {}", err))?;
+
+        metadata::apply_at(self.feature_flags, metadata, parent, &file_name)
     }
 }