From 5421a5db77247fc0be71d3892f9d06c290dcb535 Mon Sep 17 00:00:00 2001 From: Anton Hermann Date: Sun, 31 Oct 2021 20:52:32 +0100 Subject: [PATCH] Implement tree output for 'list' command --- Cargo.lock | 7 ++++ Cargo.toml | 1 + src/commands.rs | 9 ++-- src/list.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 118 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75733f1..8bcd69d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -249,6 +249,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "lzma-sys" version = "0.1.17" @@ -303,6 +309,7 @@ dependencies = [ "fs-err", "infer", "libc", + "linked-hash-map", "once_cell", "rand", "tar", diff --git a/Cargo.toml b/Cargo.toml index f290ac0..f5dd6e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ xz2 = "0.1.6" zip = { version = "0.5.13", default-features = false, features = ["deflate-miniz"] } flate2 = { version = "1.0.22", default-features = false, features = ["zlib"] } zstd = { version = "0.9.0", default-features = false, features = ["thin"] } +linked-hash-map = "0.5.4" [build-dependencies] clap = "=3.0.0-beta.5" diff --git a/src/commands.rs b/src/commands.rs index bad5775..18745c6 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -209,7 +209,10 @@ pub fn run(args: Opts, question_policy: QuestionPolicy) -> crate::Result<()> { let list_options = ListOptions { tree }; - for (archive_path, formats) in files.iter().zip(formats) { + for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() { + if i > 0 { + println!(); + } list_archive_contents(archive_path, formats, list_options)?; } } @@ -425,7 +428,7 @@ fn list_archive_contents( if let [Zip] = *formats.as_slice() { let zip_archive = zip::ZipArchive::new(reader)?; let files = crate::archive::zip::list_archive(zip_archive)?; - list::list_files(files, list_options); + list::list_files(archive_path, files, list_options); return Ok(()); } @@ -483,6 +486,6 @@ fn list_archive_contents( panic!("Not an archive! This should never happen, if it does, something is wrong with `CompressionFormat::is_archive()`. Please report this error!"); } }; - list::list_files(files, list_options); + list::list_files(archive_path, files, list_options); Ok(()) } diff --git a/src/list.rs b/src/list.rs index f75f359..dac65f7 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,6 +1,7 @@ //! Implementation of the 'list' command, print list of files in an archive -use std::path::PathBuf; +use self::tree::Tree; +use std::path::{Path, PathBuf}; /// Options controlling how archive contents should be listed #[derive(Debug, Clone, Copy)] @@ -17,12 +18,112 @@ pub struct FileInArchive { } /// Actually print the files -pub fn list_files(files: Vec, list_options: ListOptions) { +pub fn list_files(archive: &Path, files: Vec, list_options: ListOptions) { + println!("{}:", archive.display()); if list_options.tree { - todo!("Implement tree view"); + let tree: Tree = files.into_iter().collect(); + tree.print(); } else { for file in files { println!("{}", file.path.display()); } } } + +mod tree { + use super::FileInArchive; + use linked_hash_map::LinkedHashMap; + use std::ffi::OsString; + use std::iter::FromIterator; + use std::path; + + const TREE_PREFIX_EMPTY: &str = " "; + const TREE_PREFIX_LINE: &str = "│ "; + const TREE_FINAL_BRANCH: &str = "├── "; + const TREE_FINAL_LAST: &str = "└── "; + + #[derive(Debug, Default)] + pub struct Tree { + file: Option, + children: LinkedHashMap, + } + impl Tree { + /// Insert a file into the tree + pub fn insert(&mut self, file: FileInArchive) { + self.insert_(file.clone(), file.path.iter()); + } + /// Insert file by traversing the tree recursively + fn insert_(&mut self, file: FileInArchive, mut path: path::Iter) { + // Are there more components in the path? -> traverse tree further + if let Some(part) = path.next() { + // Either insert into an existing child node or create a new one + if let Some(t) = self.children.get_mut(part) { + t.insert_(file, path) + } else { + let mut child = Tree::default(); + child.insert_(file, path); + self.children.insert(part.to_os_string(), child); + } + } else { + // `path` was empty -> we reached our destination and can insert + // `file`, assuming there is no file already there (which meant + // there were 2 files with the same name in the same directory + // which should be impossible in any sane file system) + match &self.file { + None => self.file = Some(file), + Some(file) => { + eprintln!( + "[warning] multiple files with the same name in a single directory ({})", + file.path.display() + ) + } + } + } + } + + /// Print the file tree using Unicode line characters + pub fn print(&self) { + for (i, (name, subtree)) in self.children.iter().enumerate() { + subtree.print_(name, String::new(), i == self.children.len() - 1); + } + } + /// Print the tree by traversing it recursively + fn print_(&self, name: &OsString, mut prefix: String, last: bool) { + // Convert `name` to valid unicode + let name = name.to_string_lossy(); + + // If there are no further elements in the parent directory, add + // "└── " to the prefix, otherwise add "├── " + let final_part = match last { + true => TREE_FINAL_LAST, + false => TREE_FINAL_BRANCH, + }; + + if let Some(_file) = &self.file { + println!("{}{}{}", prefix, final_part, name); + } else { + println!("{}{}[{}]", prefix, final_part, name); + } + // Construct prefix for children, adding either a line if this isn't + // the last entry in the parent dir or empty space if it is. + prefix.push_str(match last { + true => TREE_PREFIX_EMPTY, + false => TREE_PREFIX_LINE, + }); + // Recursively print all children + for (i, (name, subtree)) in self.children.iter().enumerate() { + subtree.print_(name, prefix.clone(), i == self.children.len() - 1); + } + } + } + + impl FromIterator for Tree { + fn from_iter>(iter: I) -> Self { + let mut tree = Self::default(); + for file in iter { + tree.insert(file); + } + tree + } + } +}