Sfoglia il codice sorgente

Merge branch 'child-nodes'

Ben S 10 anni fa
parent
commit
eee49ece04
13 ha cambiato i file con 563 aggiunte e 487 eliminazioni
  1. 5 0
      .travis.yml
  2. 91 55
      Cargo.lock
  3. 28 18
      src/dir.rs
  4. 1 9
      src/feature/mod.rs
  5. 238 0
      src/feature/xattr.rs
  6. 0 133
      src/feature/xattr_darwin.rs
  7. 0 32
      src/feature/xattr_dummy.rs
  8. 0 122
      src/feature/xattr_linux.rs
  9. 13 32
      src/file.rs
  10. 12 6
      src/main.rs
  11. 6 6
      src/options.rs
  12. 159 72
      src/output/details.rs
  13. 10 2
      src/output/grid_details.rs

+ 5 - 0
.travis.yml

@@ -1,3 +1,8 @@
+before_install:
+  - sudo add-apt-repository --yes ppa:kubuntu-ppa/backports
+  - sudo apt-get update -qq
+  - sudo apt-get install cmake
+sudo: true
 language: rust
 rust: nightly
 

+ 91 - 55
Cargo.lock

@@ -5,18 +5,27 @@ dependencies = [
  "ansi_term 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "datetime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "getopts 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "git2 0.2.13 (git+https://github.com/alexcrichton/git2-rs.git)",
- "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "getopts 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "git2 0.3.0 (git+https://github.com/alexcrichton/git2-rs.git)",
+ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "locale 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "natord 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "num_cpus 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "number_prefix 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "pad 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "term_grid 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "threadpool 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "unicode-width 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "users 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-width 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "users 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "advapi32-sys"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -24,7 +33,7 @@ name = "aho-corasick"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "memchr 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -42,13 +51,21 @@ name = "byteorder"
 version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "cmake"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "gcc 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "datetime"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "locale 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "num 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num 0.1.27 (registry+https://github.com/rust-lang/crates.io-index)",
  "pad 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "regex 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
  "regex_macros 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -57,12 +74,16 @@ dependencies = [
 
 [[package]]
 name = "gcc"
-version = "0.3.9"
+version = "0.3.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "advapi32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
 
 [[package]]
 name = "getopts"
-version = "0.2.11"
+version = "0.2.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "log 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -70,28 +91,29 @@ dependencies = [
 
 [[package]]
 name = "git2"
-version = "0.2.13"
-source = "git+https://github.com/alexcrichton/git2-rs.git#889cf3dd62bcf8406d7c5381699467cdcb79d55e"
+version = "0.3.0"
+source = "git+https://github.com/alexcrichton/git2-rs.git#cbe8e1a65ac9b16bc05137f80673e74c4d36f6e5"
 dependencies = [
  "bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "libgit2-sys 0.2.18 (git+https://github.com/alexcrichton/git2-rs.git)",
- "url 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libgit2-sys 0.3.2 (git+https://github.com/alexcrichton/git2-rs.git)",
+ "url 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "libc"
-version = "0.1.8"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "libgit2-sys"
-version = "0.2.18"
-source = "git+https://github.com/alexcrichton/git2-rs.git#889cf3dd62bcf8406d7c5381699467cdcb79d55e"
+version = "0.3.2"
+source = "git+https://github.com/alexcrichton/git2-rs.git#cbe8e1a65ac9b16bc05137f80673e74c4d36f6e5"
 dependencies = [
- "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "libssh2-sys 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)",
- "libz-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cmake 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libssh2-sys 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libz-sys 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "openssl-sys 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "pkg-config 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -101,26 +123,28 @@ name = "libressl-pnacl-sys"
 version = "2.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "pnacl-build-helper 1.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pnacl-build-helper 1.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "libssh2-sys"
-version = "0.1.26"
+version = "0.1.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "libz-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cmake 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libz-sys 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "openssl-sys 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "pkg-config 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "libz-sys"
-version = "0.1.6"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "gcc 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "pkg-config 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -129,8 +153,8 @@ name = "locale"
 version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "num 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num 0.1.27 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -138,7 +162,7 @@ name = "log"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -148,24 +172,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "memchr"
-version = "0.1.3"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "natord"
-version = "1.0.8"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "num"
-version = "0.1.25"
+version = "0.1.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "rand 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "rustc-serialize 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -173,7 +197,7 @@ name = "num_cpus"
 version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -181,7 +205,7 @@ name = "number_prefix"
 version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "num 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num 0.1.27 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -189,8 +213,8 @@ name = "openssl-sys"
 version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "gcc 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "gcc 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "libressl-pnacl-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "pkg-config 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -200,7 +224,7 @@ name = "pad"
 version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "unicode-width 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-width 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -210,7 +234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "pnacl-build-helper"
-version = "1.4.5"
+version = "1.4.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "tempdir 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -218,10 +242,12 @@ dependencies = [
 
 [[package]]
 name = "rand"
-version = "0.3.8"
+version = "0.3.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "advapi32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -230,13 +256,13 @@ version = "0.1.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "aho-corasick 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "memchr 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex-syntax 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex-syntax 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "regex-syntax"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -249,7 +275,7 @@ dependencies = [
 
 [[package]]
 name = "rustc-serialize"
-version = "0.3.15"
+version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -257,7 +283,7 @@ name = "tempdir"
 version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "rand 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -265,7 +291,7 @@ name = "term_grid"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "unicode-width 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-width 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -283,23 +309,33 @@ dependencies = [
 
 [[package]]
 name = "unicode-width"
-version = "0.1.2"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "url"
-version = "0.2.36"
+version = "0.2.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "rustc-serialize 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "users"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "winapi"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winapi-build"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+

+ 28 - 18
src/dir.rs

@@ -1,6 +1,7 @@
 use std::io;
 use std::fs;
 use std::path::{Path, PathBuf};
+use std::slice::Iter as SliceIter;
 
 use feature::Git;
 use file::{File, fields};
@@ -22,31 +23,26 @@ impl Dir {
 
     /// Create a new Dir object filled with all the files in the directory
     /// pointed to by the given path. Fails if the directory can't be read, or
-    /// isn't actually a directory.
+    /// isn't actually a directory, or if there's an IO error that occurs
+    /// while scanning.
     pub fn readdir(path: &Path, git: bool) -> io::Result<Dir> {
-        fs::read_dir(path).map(|dir_obj| Dir {
-            contents: dir_obj.map(|entry| entry.unwrap().path()).collect(),
+        let reader = try!(fs::read_dir(path));
+        let contents = try!(reader.map(|e| e.map(|e| e.path())).collect());
+
+        Ok(Dir {
+            contents: contents,
             path: path.to_path_buf(),
             git: if git { Git::scan(path).ok() } else { None },
         })
     }
 
-    /// Produce a vector of File objects from an initialised directory,
-    /// printing out an error if any of the Files fail to be created.
-    ///
-    /// Passing in `recurse` means that any directories will be scanned for
-    /// their contents, as well.
-    pub fn files(&self, recurse: bool) -> Vec<File> {
-        let mut files = vec![];
-
-        for path in self.contents.iter() {
-            match File::from_path(path, Some(self), recurse) {
-                Ok(file) => files.push(file),
-                Err(e)   => println!("{}: {}", path.display(), e),
-            }
+    /// Produce an iterator of IO results of trying to read all the files in
+    /// this directory.
+    pub fn files<'dir>(&'dir self) -> Files<'dir> {
+        Files {
+            inner: self.contents.iter(),
+            dir: &self,
         }
-
-        files
     }
 
     /// Whether this directory contains a file with the given path.
@@ -73,3 +69,17 @@ impl Dir {
         }
     }
 }
+
+
+pub struct Files<'dir> {
+    inner: SliceIter<'dir, PathBuf>,
+    dir: &'dir Dir,
+}
+
+impl<'dir> Iterator for Files<'dir> {
+    type Item = Result<File<'dir>, (PathBuf, io::Error)>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.inner.next().map(|path| File::from_path(path, Some(self.dir)).map_err(|t| (path.clone(), t)))
+    }
+}

+ 1 - 9
src/feature/mod.rs

@@ -1,13 +1,5 @@
 // Extended attribute support
-
-#[cfg(target_os = "macos")] mod xattr_darwin;
-#[cfg(target_os = "macos")] pub use self::xattr_darwin::Attribute;
-
-#[cfg(target_os = "linux")] mod xattr_linux;
-#[cfg(target_os = "linux")] pub use self::xattr_linux::Attribute;
-
-#[cfg(not(any(target_os = "macos", target_os = "linux")))] mod xattr_dummy;
-#[cfg(not(any(target_os = "macos", target_os = "linux")))] pub use self::xattr_dummy::Attribute;
+pub mod xattr;
 
 // Git support
 

+ 238 - 0
src/feature/xattr.rs

@@ -0,0 +1,238 @@
+//! Extended attribute support for Darwin and Linux systems.
+extern crate libc;
+
+use std::io;
+use std::path::Path;
+
+
+pub const ENABLED: bool = cfg!(feature="git") && cfg!(any(target_os="macos", target_os="linux"));
+
+pub trait FileAttributes {
+    fn attributes(&self) -> io::Result<Vec<Attribute>>;
+    fn symlink_attributes(&self) -> io::Result<Vec<Attribute>>;
+}
+
+impl FileAttributes for Path {
+    fn attributes(&self) -> io::Result<Vec<Attribute>> {
+        list_attrs(lister::Lister::new(FollowSymlinks::Yes), &self)
+    }
+
+    fn symlink_attributes(&self) -> io::Result<Vec<Attribute>> {
+        list_attrs(lister::Lister::new(FollowSymlinks::No), &self)
+    }
+}
+
+/// Attributes which can be passed to `Attribute::list_with_flags`
+#[derive(Copy, Clone)]
+pub enum FollowSymlinks {
+    Yes,
+    No
+}
+
+/// Extended attribute
+#[derive(Debug, Clone)]
+pub struct Attribute {
+    pub name: String,
+    pub size: usize,
+}
+
+pub fn list_attrs(lister: lister::Lister, path: &Path) -> io::Result<Vec<Attribute>> {
+    let c_path = match path.as_os_str().to_cstring() {
+        Some(cstring) => cstring,
+        None => return Err(io::Error::new(io::ErrorKind::Other, "Error: path somehow contained a NUL?")),
+    };
+
+    let mut names = Vec::new();
+    let bufsize = lister.listxattr_first(&c_path);
+
+    if bufsize < 0 {
+        return Err(io::Error::last_os_error());
+    }
+    else if bufsize > 0 {
+        let mut buf = vec![0u8; bufsize as usize];
+        let err = lister.listxattr_second(&c_path, &mut buf, bufsize);
+
+        if err < 0 {
+            return Err(io::Error::last_os_error());
+        }
+
+        if err > 0 {
+            // End indicies of the attribute names
+            // the buffer contains 0-terminates c-strings
+            let idx = buf.iter().enumerate().filter_map(|(i, v)|
+                if *v == 0 { Some(i) } else { None }
+            );
+            let mut start = 0;
+
+            for end in idx {
+                let c_end = end + 1; // end of the c-string (including 0)
+                let size = lister.getxattr(&c_path, &buf[start..c_end]);
+
+                if size > 0 {
+                    names.push(Attribute {
+                        name: lister.translate_attribute_name(&buf[start..end]),
+                        size: size as usize
+                    });
+                }
+
+                start = c_end;
+            }
+
+        }
+
+    }
+    Ok(names)
+}
+
+#[cfg(target_os = "macos")]
+mod lister {
+    use std::ffi::CString;
+    use libc::{c_int, size_t, ssize_t, c_char, c_void, uint32_t};
+    use super::FollowSymlinks;
+    use std::ptr;
+
+    extern "C" {
+        fn listxattr(
+            path: *const c_char, namebuf: *mut c_char,
+            size: size_t, options: c_int
+        ) -> ssize_t;
+
+        fn getxattr(
+            path: *const c_char, name: *const c_char,
+            value: *mut c_void, size: size_t, position: uint32_t,
+            options: c_int
+        ) -> ssize_t;
+    }
+
+    pub struct Lister {
+        c_flags: c_int,
+    }
+
+    impl Lister {
+        pub fn new(do_follow: FollowSymlinks) -> Lister {
+            let c_flags: c_int = match do_follow {
+                FollowSymlinks::Yes => 0x0001,
+                FollowSymlinks::No  => 0x0000,
+            };
+
+            Lister { c_flags: c_flags }
+        }
+
+        pub fn translate_attribute_name(&self, input: &[u8]) -> String {
+            use std::str::from_utf8_unchecked;
+
+            unsafe {
+                from_utf8_unchecked(input).into()
+            }
+        }
+
+        pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
+            unsafe {
+                listxattr(c_path.as_ptr(), ptr::null_mut(), 0, self.c_flags)
+            }
+        }
+
+        pub fn listxattr_second(&self, c_path: &CString, buf: &mut Vec<u8>, bufsize: ssize_t) -> ssize_t {
+            unsafe {
+                listxattr(
+                    c_path.as_ptr(),
+                    buf.as_mut_ptr() as *mut c_char,
+                    bufsize as size_t, self.c_flags
+                )
+            }
+        }
+
+        pub fn getxattr(&self, c_path: &CString, buf: &[u8]) -> ssize_t {
+            unsafe {
+                getxattr(
+                    c_path.as_ptr(),
+                    buf.as_ptr() as *const c_char,
+                    ptr::null_mut(), 0, 0, self.c_flags
+                )
+            }
+        }
+    }
+}
+
+#[cfg(target_os = "linux")]
+mod lister {
+    use std::ffi::CString;
+    use libc::{size_t, ssize_t, c_char, c_void};
+    use super::FollowSymlinks;
+    use std::ptr;
+
+    extern "C" {
+        fn listxattr(
+            path: *const c_char, list: *mut c_char, size: size_t
+        ) -> ssize_t;
+
+        fn llistxattr(
+            path: *const c_char, list: *mut c_char, size: size_t
+        ) -> ssize_t;
+
+        fn getxattr(
+            path: *const c_char, name: *const c_char,
+            value: *mut c_void, size: size_t
+        ) -> ssize_t;
+
+        fn lgetxattr(
+            path: *const c_char, name: *const c_char,
+            value: *mut c_void, size: size_t
+        ) -> ssize_t;
+    }
+
+    pub struct Lister {
+        follow_symlinks: FollowSymlinks,
+    }
+
+    impl Lister {
+        pub fn new(follow_symlinks: FollowSymlinks) -> Lister {
+            Lister { follow_symlinks: follow_symlinks }
+        }
+
+        pub fn translate_attribute_name(&self, input: &[u8]) -> String {
+            String::from_utf8_lossy(input).into_owned()
+        }
+
+        pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
+            let listxattr = match self.follow_symlinks {
+                FollowSymlinks::Yes => listxattr,
+                FollowSymlinks::No  => llistxattr,
+            };
+
+            unsafe {
+                listxattr(c_path.as_ptr(), ptr::null_mut(), 0)
+            }
+        }
+
+        pub fn listxattr_second(&self, c_path: &CString, buf: &mut Vec<u8>, bufsize: ssize_t) -> ssize_t {
+            let listxattr = match self.follow_symlinks {
+                FollowSymlinks::Yes => listxattr,
+                FollowSymlinks::No  => llistxattr,
+            };
+
+            unsafe {
+                listxattr(
+                    c_path.as_ptr(),
+                    buf.as_mut_ptr() as *mut c_char,
+                    bufsize as size_t
+                )
+            }
+        }
+
+        pub fn getxattr(&self, c_path: &CString, buf: &[u8]) -> ssize_t {
+            let getxattr = match self.follow_symlinks {
+                FollowSymlinks::Yes => getxattr,
+                FollowSymlinks::No  => lgetxattr,
+            };
+
+            unsafe {
+                getxattr(
+                    c_path.as_ptr(),
+                    buf.as_ptr() as *const c_char,
+                    ptr::null_mut(), 0
+                )
+            }
+        }
+    }
+}

+ 0 - 133
src/feature/xattr_darwin.rs

@@ -1,133 +0,0 @@
-//! Extended attribute support for darwin
-extern crate libc;
-
-use std::io;
-use std::mem;
-use std::path::Path;
-use std::ptr;
-
-use self::libc::{c_int, size_t, ssize_t, c_char, c_void, uint32_t};
-
-
-/// Don't follow symbolic links
-const XATTR_NOFOLLOW: c_int = 0x0001;
-
-/// Expose HFS Compression extended attributes
-const XATTR_SHOWCOMPRESSION: c_int = 0x0020;
-
-
-extern "C" {
-    fn listxattr(path: *const c_char, namebuf: *mut c_char,
-                 size: size_t, options: c_int) -> ssize_t;
-    fn getxattr(path: *const c_char, name: *const c_char,
-                value: *mut c_void, size: size_t, position: uint32_t,
-                options: c_int) -> ssize_t;
-}
-
-
-/// Attributes which can be passed to `Attribute::list_with_flags`
-#[derive(Copy, Clone)]
-pub enum ListFlags {
-    /// Don't follow symbolic links
-    NoFollow = XATTR_NOFOLLOW as isize,
-    /// Expose HFS Compression extended attributes
-    ShowCompression = XATTR_SHOWCOMPRESSION as isize
-}
-
-
-/// Extended attribute
-#[derive(Debug, Clone)]
-pub struct Attribute {
-    name: String,
-    size: usize,
-}
-
-impl Attribute {
-    /// Lists the extended attribute of `path`.
-    /// Does follow symlinks by default.
-    pub fn list_attrs(path: &Path, flags: &[ListFlags]) -> io::Result<Vec<Attribute>> {
-        let mut c_flags: c_int = 0;
-        for &flag in flags.iter() {
-            c_flags |= flag as c_int
-        }
-
-        let c_path = match path.as_os_str().to_cstring() {
-            Some(cstring) => cstring,
-            None => return Err(io::Error::new(io::ErrorKind::Other, "could not read extended attributes")),
-        };
-
-        let bufsize = unsafe {
-            listxattr(c_path.as_ptr(), ptr::null_mut(), 0, c_flags)
-        };
-
-        if bufsize > 0 {
-            let mut buf = vec![0u8; bufsize as usize];
-            let err = unsafe { listxattr(
-                c_path.as_ptr(),
-                buf.as_mut_ptr() as *mut c_char,
-                bufsize as size_t, c_flags
-            )};
-            if err > 0 {
-                // End indicies of the attribute names
-                // the buffer contains 0-terminates c-strings
-                let idx = buf.iter().enumerate().filter_map(|(i, v)|
-                    if *v == 0 { Some(i) } else { None }
-                );
-                let mut names = Vec::new();
-                let mut start = 0;
-                for end in idx {
-                    let c_end = end + 1; // end of the c-string (including 0)
-                    let size = unsafe {
-                        getxattr(
-                            c_path.as_ptr(),
-                            buf[start..c_end].as_ptr() as *const c_char,
-                            ptr::null_mut(), 0, 0, c_flags
-                        )
-                    };
-                    if size > 0 {
-                        names.push(Attribute {
-                            name: unsafe {
-                                // buf is guaranteed to contain valid utf8 strings
-                                // see man listxattr
-                                mem::transmute::<&[u8], &str>(&buf[start..end]).to_string()
-                            },
-                            size: size as usize
-                        });
-                    }
-                    start = c_end;
-                }
-                Ok(names)
-            } else {
-                Err(io::Error::new(io::ErrorKind::Other, "could not read extended attributes"))
-            }
-        } else {
-            Err(io::Error::new(io::ErrorKind::Other, "could not read extended attributes"))
-        }
-    }
-
-    /// Getter for name
-    pub fn name(&self) -> &str {
-        &self.name
-    }
-
-    /// Getter for size
-    pub fn size(&self) -> usize {
-        self.size
-    }
-
-    /// Lists the extended attributes.
-    /// Follows symlinks like `metadata`
-    pub fn list(path: &Path) -> io::Result<Vec<Attribute>> {
-        Attribute::list_attrs(path, &[])
-    }
-    /// Lists the extended attributes.
-    /// Does not follow symlinks like `symlink_metadata`
-    pub fn llist(path: &Path) -> io::Result<Vec<Attribute>> {
-        Attribute::list_attrs(path, &[ListFlags::NoFollow])
-    }
-
-    /// Returns true if the extended attribute feature is implemented on this platform.
-    #[inline(always)]
-    pub fn feature_implemented() -> bool { true }
-}
-

+ 0 - 32
src/feature/xattr_dummy.rs

@@ -1,32 +0,0 @@
-use std::io;
-use std::path::Path;
-
-#[derive(Clone)]
-pub struct Attribute;
-
-impl Attribute {
-
-    /// Getter for name
-    pub fn name(&self) -> &str {
-        unimplemented!()
-    }
-
-    /// Getter for size
-    pub fn size(&self) -> usize {
-        unimplemented!()
-    }
-
-    /// Lists the extended attributes. Follows symlinks like `metadata`
-    pub fn list(_: &Path) -> io::Result<Vec<Attribute>> {
-        Ok(Vec::new())
-    }
-
-    /// Lists the extended attributes. Does not follow symlinks like `symlink_metadata`
-    pub fn llist(_: &Path) -> io::Result<Vec<Attribute>> {
-        Ok(Vec::new())
-    }
-
-    pub fn feature_implemented() -> bool { false }
-}
-
-

+ 0 - 122
src/feature/xattr_linux.rs

@@ -1,122 +0,0 @@
-//! Extended attribute support for darwin
-extern crate libc;
-
-use std::ffi::CString;
-use std::io;
-use std::path::Path;
-use std::ptr;
-
-use self::libc::{size_t, ssize_t, c_char, c_void};
-
-
-extern "C" {
-    fn listxattr(path: *const c_char, list: *mut c_char, size: size_t) -> ssize_t;
-    fn llistxattr(path: *const c_char, list: *mut c_char, size: size_t) -> ssize_t;
-    fn getxattr(path: *const c_char, name: *const c_char,
-                value: *mut c_void, size: size_t
-    ) -> ssize_t;
-    fn lgetxattr(path: *const c_char, name: *const c_char,
-                value: *mut c_void, size: size_t
-    ) -> ssize_t;
-}
-
-
-/// Attributes which can be passed to `Attribute::list_with_flags`
-#[derive(Copy, Clone)]
-pub enum FollowSymlinks {
-    Yes,
-    No
-}
-
-
-/// Extended attribute
-#[derive(Debug, Clone)]
-pub struct Attribute {
-    name: String,
-    size: usize,
-}
-
-impl Attribute {
-    /// Lists the extended attribute of `path`.
-    /// Does follow symlinks by default.
-    pub fn list_attrs(path: &Path, do_follow: FollowSymlinks) -> io::Result<Vec<Attribute>> {
-        let (listxattr, getxattr) = match do_follow {
-            FollowSymlinks::Yes => (listxattr, getxattr),
-            FollowSymlinks::No => (llistxattr, lgetxattr),
-        };
-
-        let c_path = match path.as_os_str().to_cstring() {
-            Some(cstring) => cstring,
-            None => return Err(io::Error::new(io::ErrorKind::Other, "could not read extended attributes")),
-        };
-
-        let bufsize = unsafe {
-            listxattr(c_path.as_ptr(), ptr::null_mut(), 0)
-        };
-
-        if bufsize > 0 {
-            let mut buf = vec![0u8; bufsize as usize];
-            let err = unsafe { listxattr(
-                c_path.as_ptr(),
-                buf.as_mut_ptr() as *mut c_char,
-                bufsize as size_t
-            )};
-            if err > 0 {
-                // End indicies of the attribute names
-                // the buffer contains 0-terminates c-strings
-                let idx = buf.iter().enumerate().filter_map(|(i, v)|
-                    if *v == 0 { Some(i) } else { None }
-                );
-                let mut names = Vec::new();
-                let mut start = 0;
-                for end in idx {
-                    let c_end = end + 1; // end of the c-string (including 0)
-                    let size = unsafe {
-                        getxattr(
-                            c_path.as_ptr(),
-                            buf[start..c_end].as_ptr() as *const c_char,
-                            ptr::null_mut(), 0
-                        )
-                    };
-                    if size > 0 {
-                        names.push(Attribute {
-                            name: String::from_utf8_lossy(&buf[start..end]).into_owned(),
-                            size: size as usize
-                        });
-                    }
-                    start = c_end;
-                }
-                Ok(names)
-            } else {
-                Err(io::Error::new(io::ErrorKind::Other, "could not read extended attributes"))
-            }
-        } else {
-            Err(io::Error::new(io::ErrorKind::Other, "could not read extended attributes"))
-        }
-    }
-
-    /// Getter for name
-    pub fn name(&self) -> &str {
-        &self.name
-    }
-
-    /// Getter for size
-    pub fn size(&self) -> usize {
-        self.size
-    }
-
-    /// Lists the extended attributes.
-    /// Follows symlinks like `metadata`
-    pub fn list(path: &Path) -> io::Result<Vec<Attribute>> {
-        Attribute::list_attrs(path, FollowSymlinks::Yes)
-    }
-    /// Lists the extended attributes.
-    /// Does not follow symlinks like `symlink_metadata`
-    pub fn llist(path: &Path) -> io::Result<Vec<Attribute>> {
-        Attribute::list_attrs(path, FollowSymlinks::No)
-    }
-
-    /// Returns true if the extended attribute feature is implemented on this platform.
-    #[inline(always)]
-    pub fn feature_implemented() -> bool { true }
-}

+ 13 - 32
src/file.rs

@@ -12,7 +12,6 @@ use unicode_width::UnicodeWidthStr;
 
 use dir::Dir;
 use options::TimeType;
-use feature::Attribute;
 
 use self::fields as f;
 
@@ -27,27 +26,23 @@ use self::fields as f;
 pub struct File<'dir> {
 
     /// This file's name, as a UTF-8 encoded String.
-    pub name:  String,
+    pub name: String,
 
     /// The file's name's extension, if present, extracted from the name. This
     /// is queried a lot, so it's worth being cached.
-    pub ext:   Option<String>,
+    pub ext: Option<String>,
 
     /// The path that begat this file. Even though the file's name is
     /// extracted, the path needs to be kept around, as certain operations
     /// involve looking up the file's absolute location (such as the Git
     /// status, or searching for compiled files).
-    pub path:  PathBuf,
+    pub path: PathBuf,
 
     /// A cached `metadata` call for this file. This is queried multiple
     /// times, and is *not* cached by the OS, as it could easily change
     /// between invocations - but exa is so short-lived it's better to just
     /// cache it.
-    pub metadata:  fs::Metadata,
-
-    /// List of this file's extended attributes. These are only loaded if the
-    /// `xattr` feature is in use.
-    pub xattrs: Vec<Attribute>,
+    pub metadata: fs::Metadata,
 
     /// A reference to the directory that contains this file, if present.
     ///
@@ -57,11 +52,7 @@ pub struct File<'dir> {
     /// However, *directories* that get passed in will produce files that
     /// contain a reference to it, which is used in certain operations (such
     /// as looking up a file's Git status).
-    pub dir:   Option<&'dir Dir>,
-
-    /// If this `File` is also a directory, then this field is the same file
-    /// as a `Dir`.
-    pub this:  Option<Dir>,
+    pub dir: Option<&'dir Dir>,
 }
 
 impl<'dir> File<'dir> {
@@ -70,32 +61,20 @@ impl<'dir> File<'dir> {
     ///
     /// This uses `symlink_metadata` instead of `metadata`, which doesn't
     /// follow symbolic links.
-    pub fn from_path(path: &Path, parent: Option<&'dir Dir>, recurse: bool) -> io::Result<File<'dir>> {
-        fs::symlink_metadata(path).map(|metadata| File::with_metadata(metadata, path, parent, recurse))
+    pub fn from_path(path: &Path, parent: Option<&'dir Dir>) -> io::Result<File<'dir>> {
+        fs::symlink_metadata(path).map(|metadata| File::with_metadata(metadata, path, parent))
     }
 
     /// Create a new File object from the given metadata result, and other data.
-    pub fn with_metadata(metadata: fs::Metadata, path: &Path, parent: Option<&'dir Dir>, recurse: bool) -> File<'dir> {
+    pub fn with_metadata(metadata: fs::Metadata, path: &Path, parent: Option<&'dir Dir>) -> File<'dir> {
         let filename = path_filename(path);
 
-        // If we are recursing, then the `this` field contains a Dir object
-        // that represents the current File as a directory, if it is a
-        // directory. This is used for the --tree option.
-        let this = if recurse && metadata.is_dir() {
-            Dir::readdir(path, false).ok()
-        }
-        else {
-            None
-        };
-
         File {
             path:   path.to_path_buf(),
             dir:    parent,
             metadata:   metadata,
             ext:    ext(&filename),
-            xattrs: Attribute::llist(path).unwrap_or(Vec::new()),
             name:   filename.to_string(),
-            this:   this,
         }
     }
 
@@ -104,6 +83,10 @@ impl<'dir> File<'dir> {
         self.metadata.is_dir()
     }
 
+    pub fn to_dir(&self) -> io::Result<Dir> {
+        Dir::readdir(&*self.path, false)
+    }
+
     /// Whether this file is a regular file on the filesystem - that is, not a
     /// directory, a link, or anything else treated specially.
     pub fn is_file(&self) -> bool {
@@ -199,9 +182,7 @@ impl<'dir> File<'dir> {
                 dir:    self.dir,
                 metadata:   metadata,
                 ext:    ext(&filename),
-                xattrs: Attribute::list(&target_path).unwrap_or(Vec::new()),
                 name:   filename.to_string(),
-                this:   None,
             })
         }
         else {
@@ -316,7 +297,7 @@ impl<'dir> File<'dir> {
             other_read:     has_bit(unix::fs::OTHER_READ),
             other_write:    has_bit(unix::fs::OTHER_WRITE),
             other_execute:  has_bit(unix::fs::OTHER_EXECUTE),
-            attribute:      !self.xattrs.is_empty()
+            attribute:      false, // !self.xattrs.is_empty()
         }
     }
 

+ 12 - 6
src/main.rs

@@ -1,3 +1,4 @@
+#![feature(iter_arith)]
 #![feature(convert, fs_mode)]
 #![feature(slice_splits, vec_resize)]
 
@@ -89,11 +90,8 @@ impl<'dir> Exa<'dir> {
                 let path = Path::new(&*file);
                 let _ = tx.send(match fs::metadata(&path) {
                     Ok(metadata) => {
-                        if !metadata.is_dir() {
-                            StatResult::File(File::with_metadata(metadata, &path, None, false))
-                        }
-                        else if is_tree {
-                            StatResult::File(File::with_metadata(metadata, &path, None, true))
+                        if is_tree || !metadata.is_dir() {
+                            StatResult::File(File::with_metadata(metadata, &path, None))
                         }
                         else {
                             StatResult::Dir(path.to_path_buf())
@@ -146,7 +144,15 @@ impl<'dir> Exa<'dir> {
 
             match Dir::readdir(&dir_path, self.options.should_scan_for_git()) {
                 Ok(ref dir) => {
-                    let mut files = dir.files(false);
+                    let mut files = Vec::new();
+
+                    for file in dir.files() {
+                        match file {
+                            Ok(file) => files.push(file),
+                            Err((path, e))   => println!("[{}: {}]", path.display(), e),
+                        }
+                    }
+
                     self.options.transform_files(&mut files);
 
                     // When recursing, add any directories to the dirs stack

+ 6 - 6
src/options.rs

@@ -11,7 +11,7 @@ use colours::Colours;
 use column::Column;
 use column::Column::*;
 use dir::Dir;
-use feature::Attribute;
+use feature::xattr;
 use file::File;
 use output::{Grid, Details, GridDetails, Lines};
 use term::dimensions;
@@ -62,7 +62,7 @@ impl Options {
             opts.optflag("", "git", "show git status");
         }
 
-        if Attribute::feature_implemented() {
+        if xattr::ENABLED {
             opts.optflag("@", "extended", "display extended attribute keys and sizes in long (-l) output");
         }
 
@@ -281,7 +281,7 @@ impl View {
                     columns: Some(try!(Columns::deduce(matches))),
                     header: matches.opt_present("header"),
                     recurse: dir_action.recurse_options().map(|o| (o, filter)),
-                    xattr: Attribute::feature_implemented() && matches.opt_present("extended"),
+                    xattr: xattr::ENABLED && matches.opt_present("extended"),
                     colours: if dimensions().is_some() { Colours::colourful() } else { Colours::plain() },
                 };
 
@@ -302,7 +302,7 @@ impl View {
             else if matches.opt_present("level") && !matches.opt_present("recurse") && !matches.opt_present("tree") {
                 Err(Useless2("level", "recurse", "tree"))
             }
-            else if Attribute::feature_implemented() && matches.opt_present("extended") {
+            else if xattr::ENABLED && matches.opt_present("extended") {
                 Err(Useless("extended", false, "long"))
             }
             else {
@@ -640,7 +640,7 @@ impl Columns {
 mod test {
     use super::Options;
     use super::Misfire;
-    use feature::Attribute;
+    use feature::xattr;
 
     fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
         match misfire {
@@ -742,7 +742,7 @@ mod test {
 
     #[test]
     fn extended_without_long() {
-        if Attribute::feature_implemented() {
+        if xattr::ENABLED {
             let opts = Options::getopts(&[ "--extended".to_string() ]);
             assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
         }

+ 159 - 72
src/output/details.rs

@@ -1,10 +1,12 @@
-use std::iter::repeat;
+use std::error::Error;
+use std::io;
+use std::path::PathBuf;
 use std::string::ToString;
 
 use colours::Colours;
 use column::{Alignment, Column, Cell};
 use dir::Dir;
-use feature::Attribute;
+use feature::xattr::{Attribute, FileAttributes};
 use file::fields as f;
 use file::File;
 use options::{Columns, FileFilter, RecurseOptions, SizeFormat};
@@ -75,7 +77,7 @@ impl Details {
 
         // Then add files to the table and print it out.
         self.add_files_to_table(&mut table, files, 0);
-        for cell in table.print_table(self.xattr, self.recurse.is_some()) {
+        for cell in table.print_table() {
             println!("{}", cell.text);
         }
     }
@@ -84,26 +86,82 @@ impl Details {
     /// is present.
     fn add_files_to_table<U: Users>(&self, table: &mut Table<U>, src: &[File], depth: usize) {
         for (index, file) in src.iter().enumerate() {
-            table.add_file(file, depth, index == src.len() - 1, true);
+            let mut xattrs = Vec::new();
+            let mut errors = Vec::new();
+
+            let has_xattrs = match file.path.attributes() {
+                Ok(xs) => {
+                    let r = !xs.is_empty();
+                    if self.xattr {
+                        for xattr in xs {
+                            xattrs.push(xattr);
+                        }
+                    }
+                    r
+                },
+                Err(e) => {
+                    if self.xattr {
+                        errors.push((e, None));
+                    }
+                    true
+                },
+            };
+
+            table.add_file(file, depth, index == src.len() - 1, true, has_xattrs);
 
             // There are two types of recursion that exa supports: a tree
             // view, which is dealt with here, and multiple listings, which is
             // dealt with in the main module. So only actually recurse if we
             // are in tree mode - the other case will be dealt with elsewhere.
             if let Some((r, filter)) = self.recurse {
-                if r.tree == false || r.is_too_deep(depth) {
-                    continue;
+                if file.is_directory() && r.tree && !r.is_too_deep(depth) {
+
+                    // Use the filter to remove unwanted files *before* expanding
+                    // them, so we don't examine any directories that wouldn't
+                    // have their contents listed anyway.
+                    match file.to_dir() {
+                        Ok(ref dir) => {
+                            let mut files = Vec::new();
+
+                            for file_to_add in dir.files() {
+                                match file_to_add {
+                                    Ok(f)          => files.push(f),
+                                    Err((path, e)) => errors.push((e, Some(path)))
+                                }
+                            }
+
+                            filter.transform_files(&mut files);
+
+                            if !files.is_empty() {
+                                for xattr in xattrs {
+                                    table.add_xattr(xattr, depth + 1, false);
+                                }
+
+                                for (error, path) in errors {
+                                    table.add_error(&error, depth + 1, false, path);
+                                }
+
+                                self.add_files_to_table(table, &files, depth + 1);
+                                continue;
+                            }
+                        },
+                        Err(e) => {
+                            errors.push((e, None));
+                        },
+                    }
                 }
+            }
 
-                // Use the filter to remove unwanted files *before* expanding
-                // them, so we don't examine any directories that wouldn't
-                // have their contents listed anyway.
-                if let Some(ref dir) = file.this {
-                    let mut files = dir.files(true);
-                    filter.transform_files(&mut files);
-                    self.add_files_to_table(table, &files, depth + 1);
-                }
+            let count = xattrs.len();
+            for (index, xattr) in xattrs.into_iter().enumerate() {
+                table.add_xattr(xattr, depth + 1, errors.is_empty() && index == count - 1);
+            }
+
+            let count = errors.len();
+            for (index, (error, path)) in errors.into_iter().enumerate() {
+                table.add_error(&error, depth + 1, index == count - 1, path);
             }
+
         }
     }
 }
@@ -112,26 +170,38 @@ impl Details {
 struct Row {
 
     /// Vector of cells to display.
-    cells:    Vec<Cell>,
+    ///
+    /// Most of the rows will be files that have had their metadata
+    /// successfully queried and displayed in these cells, so this will almost
+    /// always be `Some`. It will be `None` for a row that's only displaying
+    /// an attribute or an error.
+    cells: Option<Vec<Cell>>,
+
+    // Did You Know?
+    // A Vec<Cell> and an Option<Vec<Cell>> actually have the same byte size!
 
     /// This file's name, in coloured output. The name is treated separately
     /// from the other cells, as it never requires padding.
-    name:     Cell,
+    name: Cell,
 
     /// How many directories deep into the tree structure this is. Directories
     /// on top have depth 0.
-    depth:    usize,
-
-    /// Vector of this file's extended attributes, if that feature is active.
-    attrs:    Vec<Attribute>,
+    depth: usize,
 
     /// Whether this is the last entry in the directory. This flag is used
     /// when calculating the tree view.
-    last:     bool,
+    last: bool,
+}
+
+impl Row {
 
-    /// Whether this file is a directory and has any children. Also used when
-    /// calculating the tree view.
-    children: bool,
+    /// Gets the 'width' of the indexed column, if present. If not, returns 0.
+    fn column_width(&self, index: usize) -> usize {
+        match self.cells {
+            Some(ref cells) => cells[index].length,
+            None => 0,
+        }
+    }
 }
 
 
@@ -191,30 +261,53 @@ impl<U> Table<U> where U: Users {
     pub fn add_header(&mut self) {
         let row = Row {
             depth:    0,
-            cells:    self.columns.iter().map(|c| Cell::paint(self.colours.header, c.header())).collect(),
+            cells:    Some(self.columns.iter().map(|c| Cell::paint(self.colours.header, c.header())).collect()),
             name:     Cell::paint(self.colours.header, "Name"),
             last:     false,
-            attrs:    Vec::new(),
-            children: false,
+        };
+
+        self.rows.push(row);
+    }
+
+    fn add_error(&mut self, error: &io::Error, depth: usize, last: bool, path: Option<PathBuf>) {
+        let error_message = match path {
+            Some(path) => format!("<{}: {}>", path.display(), error),
+            None       => format!("<{}>", error),
+        };
+
+        let row = Row {
+            depth:    depth,
+            cells:    None,
+            name:     Cell::paint(self.colours.broken_arrow, &error_message),
+            last:     last,
+        };
+
+        self.rows.push(row);
+    }
+
+    fn add_xattr(&mut self, xattr: Attribute, depth: usize, last: bool) {
+        let row = Row {
+            depth:    depth,
+            cells:    None,
+            name:     Cell::paint(self.colours.perms.attribute, &format!("{}\t{}", xattr.name, xattr.size)),
+            last:     last,
         };
 
         self.rows.push(row);
     }
 
     /// Get the cells for the given file, and add the result to the table.
-    pub fn add_file(&mut self, file: &File, depth: usize, last: bool, links: bool) {
-        let cells = self.cells_for_file(file);
+    fn add_file(&mut self, file: &File, depth: usize, last: bool, links: bool, xattrs: bool) {
+        let cells = self.cells_for_file(file, xattrs);
         self.add_file_with_cells(cells, file, depth, last, links)
     }
 
     pub fn add_file_with_cells(&mut self, cells: Vec<Cell>, file: &File, depth: usize, last: bool, links: bool) {
         let row = Row {
             depth:    depth,
-            cells:    cells,
+            cells:    Some(cells),
             name:     Cell { text: filename(file, &self.colours, links), length: file.file_name_width() },
             last:     last,
-            attrs:    file.xattrs.clone(),
-            children: file.this.is_some(),
         };
 
         self.rows.push(row);
@@ -222,15 +315,15 @@ impl<U> Table<U> where U: Users {
 
     /// Use the list of columns to find which cells should be produced for
     /// this file, per-column.
-    pub fn cells_for_file(&mut self, file: &File) -> Vec<Cell> {
+    pub fn cells_for_file(&mut self, file: &File, xattrs: bool) -> Vec<Cell> {
         self.columns.clone().iter()
-                    .map(|c| self.display(file, c))
+                    .map(|c| self.display(file, c, xattrs))
                     .collect()
     }
 
-    fn display(&mut self, file: &File, column: &Column) -> Cell {
+    fn display(&mut self, file: &File, column: &Column, xattrs: bool) -> Cell {
         match *column {
-            Column::Permissions    => self.render_permissions(file.permissions()),
+            Column::Permissions    => self.render_permissions(file.permissions(), xattrs),
             Column::FileSize(fmt)  => self.render_size(file.size(), fmt),
             Column::Timestamp(t)   => self.render_time(file.timestamp(t)),
             Column::HardLinks      => self.render_links(file.links()),
@@ -242,7 +335,7 @@ impl<U> Table<U> where U: Users {
         }
     }
 
-    fn render_permissions(&self, permissions: f::Permissions) -> Cell {
+    fn render_permissions(&self, permissions: f::Permissions, xattrs: bool) -> Cell {
         let c = self.colours.perms;
         let bit = |bit, chr: &'static str, style: Style| {
             if bit { style.paint(chr) } else { self.colours.punctuation.paint("-") }
@@ -272,7 +365,7 @@ impl<U> Table<U> where U: Users {
             bit(permissions.other_execute, "x", c.other_execute),
         ];
 
-        if permissions.attribute {
+        if xattrs {
             columns.push(c.attribute.paint("@"));
         }
 
@@ -388,8 +481,8 @@ impl<U> Table<U> where U: Users {
         Cell::paint(style, &*group_name)
     }
 
-    /// Print the table to standard output, consuming it in the process.
-    pub fn print_table(&self, xattr: bool, show_children: bool) -> Vec<Cell> {
+    /// Render the table as a vector of Cells, to be displayed on standard output.
+    pub fn print_table(&self) -> Vec<Cell> {
         let mut stack = Vec::new();
         let mut cells = Vec::new();
 
@@ -397,19 +490,26 @@ impl<U> Table<U> where U: Users {
         // each column, then formatting each cell in that column to be the
         // width of that one.
         let column_widths: Vec<usize> = (0 .. self.columns.len())
-            .map(|n| self.rows.iter().map(|row| row.cells[n].length).max().unwrap_or(0))
+            .map(|n| self.rows.iter().map(|row| row.column_width(n)).max().unwrap_or(0))
             .collect();
 
+        let total_width: usize = self.columns.len() + column_widths.iter().sum::<usize>();
+
         for row in self.rows.iter() {
             let mut cell = Cell::empty();
 
-            for (n, width) in column_widths.iter().enumerate() {
-                match self.columns[n].alignment() {
-                    Alignment::Left  => { cell.append(&row.cells[n]); cell.add_spaces(width - row.cells[n].length); }
-                    Alignment::Right => { cell.add_spaces(width - row.cells[n].length); cell.append(&row.cells[n]); }
-                }
+            if let Some(ref cells) = row.cells {
+                for (n, width) in column_widths.iter().enumerate() {
+                    match self.columns[n].alignment() {
+                        Alignment::Left  => { cell.append(&cells[n]); cell.add_spaces(width - cells[n].length); }
+                        Alignment::Right => { cell.add_spaces(width - cells[n].length); cell.append(&cells[n]); }
+                    }
 
-                cell.add_spaces(1);
+                    cell.add_spaces(1);
+                }
+            }
+            else {
+                cell.add_spaces(total_width)
             }
 
             let mut filename = String::new();
@@ -419,40 +519,27 @@ impl<U> Table<U> where U: Users {
             // necessary to maintain information about the previously-printed
             // lines, as the output will change based on whether the
             // *previous* entry was the last in its directory.
-            if show_children {
-                stack.resize(row.depth + 1, TreePart::Edge);
-                stack[row.depth] = if row.last { TreePart::Corner } else { TreePart::Edge };
+            stack.resize(row.depth + 1, TreePart::Edge);
+            stack[row.depth] = if row.last { TreePart::Corner } else { TreePart::Edge };
 
-                for i in 1 .. row.depth + 1 {
-                    filename.push_str(&*self.colours.punctuation.paint(stack[i].ascii_art()).to_string());
-                    filename_length += 4;
-                }
+            for i in 1 .. row.depth + 1 {
+                filename.push_str(&*self.colours.punctuation.paint(stack[i].ascii_art()).to_string());
+                filename_length += 4;
+            }
 
-                if row.children {
-                    stack[row.depth] = if row.last { TreePart::Blank } else { TreePart::Line };
-                }
+            stack[row.depth] = if row.last { TreePart::Blank } else { TreePart::Line };
 
-                // If any tree characters have been printed, then add an extra
-                // space, which makes the output look much better.
-                if row.depth != 0 {
-                    filename.push(' ');
-                    filename_length += 1;
-                }
+            // If any tree characters have been printed, then add an extra
+            // space, which makes the output look much better.
+            if row.depth != 0 {
+                filename.push(' ');
+                filename_length += 1;
             }
 
             // Print the name without worrying about padding.
             filename.push_str(&*row.name.text);
             filename_length += row.name.length;
 
-            if xattr {
-                let width = row.attrs.iter().map(|a| a.name().len()).max().unwrap_or(0);
-                for attr in row.attrs.iter() {
-                    let name = attr.name();
-                    let spaces: String = repeat(" ").take(width - name.len()).collect();
-                    filename.push_str(&*format!("\n{}{}  {}", name, spaces, attr.size()))
-                }
-            }
-
             cell.append(&Cell { text: filename, length: filename_length });
             cells.push(cell);
         }

+ 10 - 2
src/output/grid_details.rs

@@ -5,6 +5,7 @@ use term_grid as grid;
 
 use column::{Column, Cell};
 use dir::Dir;
+use feature::xattr::FileAttributes;
 use file::File;
 use output::details::{Details, Table};
 use output::grid::Grid;
@@ -15,6 +16,13 @@ pub struct GridDetails {
     pub details: Details,
 }
 
+fn file_has_xattrs(file: &File) -> bool {
+    match file.path.attributes() {
+        Ok(attrs) => !attrs.is_empty(),
+        Err(_) => false,
+    }
+}
+
 impl GridDetails {
     pub fn view(&self, dir: Option<&Dir>, files: &[File]) {
         let columns_for_dir = match self.details.columns {
@@ -23,7 +31,7 @@ impl GridDetails {
         };
 
         let mut first_table = Table::with_options(self.details.colours, columns_for_dir.clone());
-        let cells: Vec<_> = files.iter().map(|file| first_table.cells_for_file(file)).collect();
+        let cells: Vec<_> = files.iter().map(|file| first_table.cells_for_file(file, file_has_xattrs(file))).collect();
 
         let mut last_working_table = self.make_grid(1, &*columns_for_dir, files, cells.clone());
 
@@ -73,7 +81,7 @@ impl GridDetails {
             tables[index].add_file_with_cells(row, file, 0, false, false);
         }
 
-        let columns: Vec<_> = tables.iter().map(|t| t.print_table(false, false)).collect();
+        let columns: Vec<_> = tables.iter().map(|t| t.print_table()).collect();
 
         let direction = if self.grid.across { grid::Direction::LeftToRight }
                                        else { grid::Direction::TopToBottom };