1
0
Эх сурвалжийг харах

Merge branch 'awkward-file-names'

Fixes #156.
Benjamin Sago 8 жил өмнө
parent
commit
3ebc22580a

+ 33 - 0
Vagrantfile

@@ -142,6 +142,39 @@ Vagrant.configure(2) do |config|
     EOF
 
 
+    # File name testcases.
+    # bash really doesn’t want you to create a file with escaped characters
+    # in its name, so we have to resort to the echo builtin and touch!
+    #
+    # The double backslashes are not strictly necessary; without them, Ruby
+    # will interpolate them instead of bash, but because Vagrant prints out
+    # each command it runs, your *own* terminal will go “ding” from the alarm!
+    config.vm.provision :shell, privileged: false, inline: <<-EOF
+        set -xe
+        mkdir "#{test_dir}/file-names"
+
+        echo -ne "#{test_dir}/file-names/ascii: hello" | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/emoji: [🆒]"  | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/utf-8: pâté"  | xargs -0 touch
+
+        echo -ne "#{test_dir}/file-names/bell: [\\a]"         | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/backspace: [\\b]"    | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/form-feed: [\\f]"    | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/new-line: [\\n]"     | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/return: [\\r]"       | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/tab: [\\t]"          | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/vertical-tab: [\\v]" | xargs -0 touch
+
+        echo -ne "#{test_dir}/file-names/escape: [\\033]"               | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/ansi: [\\033[34mblue\\033[0m]" | xargs -0 touch
+
+        echo -ne "#{test_dir}/file-names/invalid-utf8-1: [\\xFF]"                | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/invalid-utf8-2: [\\xc3\\x28]"           | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/invalid-utf8-3: [\\xe2\\x82\\x28]"      | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/invalid-utf8-4: [\\xf0\\x28\\x8c\\x28]" | xargs -0 touch
+    EOF
+
+
     # Special file testcases.
     config.vm.provision :shell, privileged: false, inline: <<-EOF
         set -xe

+ 5 - 15
src/output/cell.rs

@@ -5,8 +5,6 @@ use std::ops::{Add, Deref, DerefMut};
 use ansi_term::{Style, ANSIString, ANSIStrings};
 use unicode_width::UnicodeWidthStr;
 
-use fs::File;
-
 
 /// An individual cell that holds text in a table, used in the details and
 /// lines views to store ANSI-terminal-formatted data before it is printed.
@@ -161,6 +159,11 @@ impl TextCellContents {
     pub fn strings(&self) -> ANSIStrings {
         ANSIStrings(&self.0)
     }
+
+    pub fn width(&self) -> DisplayWidth {
+        let foo = self.0.iter().map(|anstr| anstr.chars().count()).sum();
+        DisplayWidth(foo)
+    }
 }
 
 
@@ -180,19 +183,6 @@ impl TextCellContents {
 #[derive(PartialEq, Debug, Clone, Copy, Default)]
 pub struct DisplayWidth(usize);
 
-impl DisplayWidth {
-    pub fn from_file(file: &File, classify: bool) -> DisplayWidth {
-        let name_width = *DisplayWidth::from(&*file.name);
-        if classify {
-            if file.is_executable_file() || file.is_directory() ||
-                file.is_pipe() || file.is_link() || file.is_socket() {
-                return DisplayWidth(name_width + 1);
-            }
-        }
-        DisplayWidth(name_width)
-    }
-}
-
 impl<'a> From<&'a str> for DisplayWidth {
     fn from(input: &'a str) -> DisplayWidth {
         DisplayWidth(UnicodeWidthStr::width(input))

+ 3 - 1
src/output/colours.rs

@@ -22,6 +22,7 @@ pub struct Colours {
     pub symlink_path:     Style,
     pub broken_arrow:     Style,
     pub broken_filename:  Style,
+    pub control_char:     Style,
 }
 
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
@@ -170,7 +171,8 @@ impl Colours {
 
             symlink_path:     Cyan.normal(),
             broken_arrow:     Red.normal(),
-            broken_filename:  Red.underline()
+            broken_filename:  Red.underline(),
+            control_char:     Red.normal(),
         }
     }
 

+ 7 - 4
src/output/details.rs

@@ -306,7 +306,9 @@ impl Details {
         for (index, egg) in file_eggs.into_iter().enumerate() {
             let mut files = Vec::new();
             let mut errors = egg.errors;
-            let mut width = DisplayWidth::from_file(&egg.file, self.classify);
+
+            let filename = filename(&egg.file, &self.colours, true, self.classify);
+            let mut width = filename.width();
 
             if egg.file.dir.is_none() {
                 if let Some(parent) = egg.file.path.parent() {
@@ -315,7 +317,7 @@ impl Details {
             }
 
             let name = TextCell {
-                contents: filename(&egg.file, &self.colours, true, self.classify),
+                contents: filename,
                 width:    width,
             };
 
@@ -456,7 +458,8 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
     }
 
     pub fn filename_cell(&self, file: File, links: bool) -> TextCell {
-        let mut width = DisplayWidth::from_file(&file, self.opts.classify);
+        let filename = filename(&file, &self.opts.colours, links, self.opts.classify);
+        let mut width = filename.width();
 
         if file.dir.is_none() {
             if let Some(parent) = file.path.parent() {
@@ -465,7 +468,7 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
         }
 
         TextCell {
-            contents: filename(&file, &self.opts.colours, links, self.opts.classify),
+            contents: filename,
             width:    width,
         }
     }

+ 3 - 2
src/output/grid.rs

@@ -29,8 +29,9 @@ impl Grid {
         grid.reserve(files.len());
 
         for file in files.iter() {
-            let mut width = DisplayWidth::from_file(file, self.classify);
+            let filename = filename(file, &self.colours, false, self.classify);
 
+            let mut width = filename.width();
             if file.dir.is_none() {
                 if let Some(parent) = file.path.parent() {
                     width = width + 1 + DisplayWidth::from(parent.to_string_lossy().as_ref());
@@ -38,7 +39,7 @@ impl Grid {
             }
 
             grid.add(grid::Cell {
-                contents:  filename(file, &self.colours, false, self.classify).strings().to_string(),
+                contents:  filename.strings().to_string(),
                 width:     *width,
             });
         }

+ 44 - 2
src/output/mod.rs

@@ -1,4 +1,4 @@
-use ansi_term::Style;
+use ansi_term::{ANSIString, Style};
 
 use fs::{File, FileTarget};
 
@@ -22,6 +22,8 @@ mod tree;
 pub fn filename(file: &File, colours: &Colours, links: bool, classify: bool) -> TextCellContents {
     let mut bits = Vec::new();
 
+    // TODO: This long function could do with some splitting up.
+
     if file.dir.is_none() {
         if let Some(parent) = file.path.parent() {
             let coconut = parent.components().count();
@@ -37,7 +39,9 @@ pub fn filename(file: &File, colours: &Colours, links: bool, classify: bool) ->
     }
 
     if !file.name.is_empty() {
-        bits.push(file_colour(colours, file).paint(file.name.clone()));
+        for bit in coloured_file_name(file, colours) {
+            bits.push(bit);
+        }
     }
 
     if links && file.is_link() {
@@ -92,6 +96,44 @@ pub fn filename(file: &File, colours: &Colours, links: bool, classify: bool) ->
     bits.into()
 }
 
+/// Returns at least one ANSI-highlighted string representing this file’s
+/// name using the given set of colours.
+///
+/// Ordinarily, this will be just one string: the file’s complete name,
+/// coloured according to its file type. If the name contains control
+/// characters such as newlines or escapes, though, we can’t just print them
+/// to the screen directly, because then there’ll be newlines in weird places.
+///
+/// So in that situation, those characters will be escaped and highlighted in
+/// a different colour.
+fn coloured_file_name<'a>(file: &File, colours: &Colours) -> Vec<ANSIString<'a>> {
+    let colour = file_colour(colours, file);
+    let mut bits = Vec::new();
+
+    if file.name.chars().all(|c| c >= 0x20 as char) {
+        bits.push(colour.paint(file.name.clone()));
+    }
+    else {
+        for c in file.name.chars() {
+            // The `escape_default` method on `char` is *almost* what we want here, but
+            // it still escapes non-ASCII UTF-8 characters, which are still printable.
+
+            if c >= 0x20 as char {
+                // TODO: This allocates way too much,
+                // hence the `all` check above.
+                let mut s = String::new();
+                s.push(c);
+                bits.push(colour.paint(s));
+            } else {
+                let s = c.escape_default().collect::<String>();
+                bits.push(colours.control_char.paint(s));
+            }
+        }
+    }
+
+    bits
+}
+
 pub fn file_colour(colours: &Colours, file: &File) -> Style {
     match file {
         f if f.is_directory()        => colours.filetypes.directory,

+ 6 - 0
xtests/file_names

@@ -0,0 +1,6 @@
+ansi: [\u{1b}[34mblue\u{1b}[0m]  form-feed: [\u{c}]      return: [\r]
+ascii: hello                     invalid-utf8-1: [�]     tab: [\t]
+backspace: [\u{8}]               invalid-utf8-2: [�(]    utf-8: pâté
+bell: [\u{7}]                    invalid-utf8-3: [�(]    vertical-tab: [\u{b}]
+emoji: [🆒]                       invalid-utf8-4: [�(�(]  
+escape: [\u{1b}]                 new-line: [\n]          

+ 16 - 0
xtests/file_names_1

@@ -0,0 +1,16 @@
+ansi: [\u{1b}[34mblue\u{1b}[0m]
+ascii: hello
+backspace: [\u{8}]
+bell: [\u{7}]
+emoji: [🆒]
+escape: [\u{1b}]
+form-feed: [\u{c}]
+invalid-utf8-1: [�]
+invalid-utf8-2: [�(]
+invalid-utf8-3: [�(]
+invalid-utf8-4: [�(�(]
+new-line: [\n]
+return: [\r]
+tab: [\t]
+utf-8: pâté
+vertical-tab: [\u{b}]

+ 6 - 0
xtests/file_names_x

@@ -0,0 +1,6 @@
+ansi: [\u{1b}[34mblue\u{1b}[0m]  ascii: hello            backspace: [\u{8}]
+bell: [\u{7}]                    emoji: [🆒]              escape: [\u{1b}]
+form-feed: [\u{c}]               invalid-utf8-1: [�]     invalid-utf8-2: [�(]
+invalid-utf8-3: [�(]             invalid-utf8-4: [�(�(]  new-line: [\n]
+return: [\r]                     tab: [\t]               utf-8: pâté
+vertical-tab: [\u{b}]            

+ 4 - 0
xtests/run.sh

@@ -54,6 +54,10 @@ $exa $testcases/passwd -lgh | diff -q - $results/passwd  || exit 1
 sudo -u cassowary $exa $testcases/permissions -lghR 2>&1 | diff -q - $results/permissions_sudo  || exit 1
                   $exa $testcases/permissions -lghR 2>&1 | diff -q - $results/permissions       || exit 1
 
+# File names
+COLUMNS=80 $exa $testcases/file-names    2>&1 | diff -q - $results/file_names   || exit 1
+COLUMNS=80 $exa $testcases/file-names -x 2>&1 | diff -q - $results/file_names_x || exit 1
+           $exa $testcases/file-names -1 2>&1 | diff -q - $results/file_names_1 || exit 1
 
 # File types
 $exa $testcases/file-names-exts -1 2>&1 | diff -q - $results/file-names-exts  || exit 1