diff --git a/Cargo.lock b/Cargo.lock index 0ad3ab3b9..1efec8272 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,7 @@ dependencies = [ "unexpand 0.0.1", "unindent 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "uniq 0.0.1", + "unix_socket 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "unlink 0.0.1", "uptime 0.0.1", "users 0.0.1", @@ -185,6 +186,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "cat" version = "0.0.1" dependencies = [ + "quick-error 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unix_socket 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.1", ] @@ -725,6 +728,11 @@ dependencies = [ "uucore 0.0.1", ] +[[package]] +name = "quick-error" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "rand" version = "0.3.15" @@ -1123,6 +1131,15 @@ dependencies = [ "uucore 0.0.1", ] +[[package]] +name = "unix_socket" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "unlink" version = "0.0.1" @@ -1259,6 +1276,7 @@ dependencies = [ "checksum nix 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a0d95c5fa8b641c10ad0b8887454ebaafa3c92b5cd5350f8fc693adafd178e7b" "checksum num_cpus 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "55aabf4e2d6271a2e4e4c0f2ea1f5b07cc589cc1a9e9213013b54a76678ca4f3" "checksum pretty-bytes 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3095b93999fae14b4e0bb661c53875a441d9058b7b1a7ba2dfebc104d3776349" +"checksum quick-error 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0aad603e8d7fb67da22dbdf1f4b826ce8829e406124109e73cf1b2454b93a71c" "checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d" "checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f" "checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957" @@ -1277,6 +1295,7 @@ dependencies = [ "checksum time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "3c7ec6d62a20df54e07ab3b78b9a3932972f4b7981de295563686849eb3989af" "checksum unicode-width 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2d6722facc10989f63ee0e20a83cd4e1714a9ae11529403ac7e0afd069abc39e" "checksum unindent 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3508be0ce1bacc38d579b69bffb4b8d469f5af0c388ff4890b2b294e61671ffe" +"checksum unix_socket 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6aa2700417c405c38f5e6902d699345241c28c0b7ade4abaad71e35a87eb1564" "checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum walkdir 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c66c0b9792f0a765345452775f3adbd28dde9d33f30d13e5dcc5ae17cf6f3780" diff --git a/Cargo.toml b/Cargo.toml index 5fd1a295b..e593d4690 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -235,6 +235,9 @@ tempdir="*" unindent="*" lazy_static = "*" +[target.'cfg(unix)'.dev-dependencies] +unix_socket = "*" + [[bin]] name = "uutils" path = "src/uutils/uutils.rs" diff --git a/src/cat/Cargo.toml b/src/cat/Cargo.toml index 9b54d06b5..28d9d6ebe 100644 --- a/src/cat/Cargo.toml +++ b/src/cat/Cargo.toml @@ -8,8 +8,12 @@ name = "uu_cat" path = "cat.rs" [dependencies] +quick-error = "1.1.0" uucore = { path="../uucore" } +[target.'cfg(unix)'.dependencies] +unix_socket = "0.5.0" + [[bin]] name = "cat" path = "main.rs" diff --git a/src/cat/cat.rs b/src/cat/cat.rs index fb0c6cc16..48587ffe3 100644 --- a/src/cat/cat.rs +++ b/src/cat/cat.rs @@ -4,23 +4,127 @@ // // (c) Jordi Boggiano // (c) Evgeniy Klyuchikov +// (c) Joshua S. Miller // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // + +#[macro_use] +extern crate quick_error; +#[cfg(unix)] +extern crate unix_socket; #[macro_use] extern crate uucore; // last synced with: cat (GNU coreutils) 8.13 -use std::fs::File; -use std::io::{stdout, stdin, stderr, Write, Read, BufWriter}; +use quick_error::ResultExt; +use std::fs::{metadata, File}; +use std::io::{self, stdout, stdin, stderr, Write, Read, BufWriter}; use uucore::fs::is_stdin_interactive; +/// Unix domain socket support +#[cfg(unix)] use std::net::Shutdown; +#[cfg(unix)] use std::os::unix::fs::FileTypeExt; +#[cfg(unix)] use unix_socket::UnixStream; + static SYNTAX: &'static str = "[OPTION]... [FILE]..."; static SUMMARY: &'static str = "Concatenate FILE(s), or standard input, to standard output With no FILE, or when FILE is -, read standard input."; static LONG_HELP: &'static str = ""; + +#[derive(PartialEq)] +enum NumberingMode { + NumberNone, + NumberNonEmpty, + NumberAll, +} + + +quick_error! { + #[derive(Debug)] + enum CatError { + /// Wrapper for io::Error with path context + Input(err: io::Error, path: String) { + display("cat: {0}: {1}", path, err) + context(path: &'a str, err: io::Error) -> (err, path.to_owned()) + cause(err) + } + + /// Wrapper for io::Error with no context + Output(err: io::Error) { + display("cat: {0}", err) from() + cause(err) + } + + /// Uknown Filetype classification + UnknownFiletype(path: String) { + display("cat: {0}: unknown filetype", path) + } + + /// At least one error was encountered in reading or writing + EncounteredErrors(count: usize) { + display("cat: encountered {0} errors", count) + } + + /// Denotes an error caused by trying to `cat` a directory + IsDirectory(path: String) { + display("cat: {0}: Is a directory", path) + } + } +} + + +struct OutputOptions { + /// Line numbering mode + number: NumberingMode, + + /// Suppress repeated empty output lines + squeeze_blank: bool, + + /// display TAB characters as `tab` + show_tabs: bool, + + /// If `show_tabs == true`, this string will be printed in the + /// place of tabs + tab: String, + + /// Can be set to show characters other than '\n' a the end of + /// each line, e.g. $ + end_of_line: String, + + /// use ^ and M- notation, except for LF (\\n) and TAB (\\t) + show_nonprint: bool, +} + + +/// Represents an open file handle, stream, or other device +struct InputHandle { + reader: Box, + is_interactive: bool, +} + + +/// Concrete enum of recognized file types. +/// +/// *Note*: `cat`-ing a directory should result in an +/// CatError::IsDirectory +enum InputType { + Directory, + File, + StdIn, + SymLink, + #[cfg(unix)] BlockDevice, + #[cfg(unix)] CharacterDevice, + #[cfg(unix)] Fifo, + #[cfg(unix)] Socket, + } + + +type CatResult = Result; + + pub fn uumain(args: Vec) -> i32 { let matches = new_coreopts!(SYNTAX, SUMMARY, LONG_HELP) .optflag("A", "show-all", "equivalent to -vET") @@ -45,6 +149,7 @@ pub fn uumain(args: Vec) -> i32 { } else { NumberingMode::NumberNone }; + let show_nonprint = matches.opts_present(&["A".to_owned(), "e".to_owned(), "t".to_owned(), "v".to_owned()]); let show_ends = matches.opts_present(&["E".to_owned(), "A".to_owned(), "e".to_owned()]); @@ -55,136 +160,240 @@ pub fn uumain(args: Vec) -> i32 { files.push("-".to_owned()); } - if show_tabs || show_nonprint || show_ends || squeeze_blank || - number_mode != NumberingMode::NumberNone { - write_lines(files, - number_mode, - squeeze_blank, - show_ends, - show_tabs, - show_nonprint); + let can_write_fast = !(show_tabs + || show_nonprint + || show_ends + || squeeze_blank + || number_mode != NumberingMode::NumberNone); + + let success = if can_write_fast { + write_fast(files).is_ok() + } else { - write_fast(files); + let tab = match show_tabs { + true => "^I", + false => "\t", + }.to_owned(); + + let end_of_line = match show_ends { + true => "$\n", + false => "\n", + }.to_owned(); + + let options = OutputOptions { + end_of_line: end_of_line, + number: number_mode, + show_nonprint: show_nonprint, + show_tabs: show_tabs, + squeeze_blank: squeeze_blank, + tab: tab, + }; + + write_lines(files, &options).is_ok() + }; + + match success { + true => 0, + false => 1, } - pipe_flush!(); - - 0 } -#[derive(PartialEq)] -enum NumberingMode { - NumberNone, - NumberNonEmpty, - NumberAll, + +/// Classifies the `InputType` of file at `path` if possible +/// +/// # Arguments +/// +/// * `path` - Path on a file system to classify metadata +fn get_input_type(path: &str) -> CatResult { + if path == "-" { + return Ok(InputType::StdIn) + } + + match metadata(path).context(path)?.file_type() { + #[cfg(unix)] ft if ft.is_block_device() => Ok(InputType::BlockDevice), + #[cfg(unix)] ft if ft.is_char_device() => Ok(InputType::CharacterDevice), + #[cfg(unix)] ft if ft.is_fifo() => Ok(InputType::Fifo), + #[cfg(unix)] ft if ft.is_socket() => Ok(InputType::Socket), + ft if ft.is_dir() => Ok(InputType::Directory), + ft if ft.is_file() => Ok(InputType::File), + ft if ft.is_symlink() => Ok(InputType::SymLink), + _ => Err(CatError::UnknownFiletype(path.to_owned())) + } } -fn open(path: &str) -> Option<(Box, bool)> { +/// Returns an InputHandle from which a Reader can be accessed or an +/// error +/// +/// # Arguments +/// +/// * `path` - `InputHandler` will wrap a reader from this file path +fn open(path: &str) -> CatResult { if path == "-" { let stdin = stdin(); - return Some((Box::new(stdin) as Box, is_stdin_interactive())); + return Ok(InputHandle { + reader: Box::new(stdin) as Box, + is_interactive: is_stdin_interactive(), + }); } - match File::open(path) { - Ok(f) => Some((Box::new(f) as Box, false)), - Err(e) => { - (writeln!(&mut stderr(), "cat: {0}: {1}", path, e.to_string())).unwrap(); - None - } + match get_input_type(path)? { + InputType::Directory => Err(CatError::IsDirectory(path.to_owned())), + #[cfg(unix)] InputType::Socket => { + let socket = UnixStream::connect(path).context(path)?; + socket.shutdown(Shutdown::Write).context(path)?; + Ok(InputHandle { + reader: Box::new(socket) as Box, + is_interactive: false, + }) + }, + _ => { + let file = File::open(path).context(path)?; + Ok(InputHandle { + reader: Box::new(file) as Box, + is_interactive: false, + }) + }, } } -fn write_fast(files: Vec) { +/// Writes files to stdout with no configuration. This allows a +/// simple memory copy. Returns `Ok(())` if no errors were +/// encountered, or an error with the number of errors encountered. +/// +/// # Arguments +/// +/// * `files` - There is no short circuit when encountiner an error +/// reading a file in this vector +fn write_fast(files: Vec) -> CatResult<()> { let mut writer = stdout(); let mut in_buf = [0; 1024 * 64]; + let mut error_count = 0; - for (mut reader, _) in files.iter().filter_map(|p| open(&p[..])) { - while let Ok(n) = reader.read(&mut in_buf) { - if n == 0 { - break; - } - writer.write_all(&in_buf[..n]).unwrap(); + for file in files { + match open(&file[..]) { + Ok(mut handle) => { + while let Ok(n) = handle.reader.read(&mut in_buf) { + if n == 0 { + break; + } + writer.write_all(&in_buf[..n]).context(&file[..])?; + } + }, + Err(error) => { + writeln!(&mut stderr(), "{}", error)?; + error_count += 1; + }, } } + + match error_count { + 0 => Ok(()), + _ => Err(CatError::EncounteredErrors(error_count)), + } } -fn write_lines(files: Vec, - number: NumberingMode, - squeeze_blank: bool, - show_ends: bool, - show_tabs: bool, - show_nonprint: bool) { - // initialize end of line - let end_of_line = if show_ends { - "$\n".as_bytes() - } else { - "\n".as_bytes() - }; - // initialize tab simbol - let tab = if show_tabs { - "^I".as_bytes() - } else { - "\t".as_bytes() - }; + +/// Writes files to stdout with `options` as configuration. Returns +/// `Ok(())` if no errors were encountered, or an error with the +/// number of errors encountered. +/// +/// # Arguments +/// +/// * `files` - There is no short circuit when encountiner an error +/// reading a file in this vector +fn write_lines(files: Vec, options: &OutputOptions) -> CatResult<()> { let mut line_counter: usize = 1; + let mut error_count = 0; - for (mut reader, interactive) in files.iter().filter_map(|p| open(&p[..])) { - let mut in_buf = [0; 1024 * 31]; - let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); - let mut at_line_start = true; - let mut one_blank_kept = false; - while let Ok(n) = reader.read(&mut in_buf) { - if n == 0 { - break; - } - let in_buf = &in_buf[..n]; - let mut pos = 0; - while pos < n { - // skip empty lines enumerating them if needed - if in_buf[pos] == '\n' as u8 { - if !at_line_start || !squeeze_blank || !one_blank_kept { - one_blank_kept = true; - if at_line_start && number == NumberingMode::NumberAll { - (write!(&mut writer, "{0:6}\t", line_counter)).unwrap(); - line_counter += 1; - } - writer.write_all(end_of_line).unwrap(); - if interactive { - writer.flush().unwrap(); - } - } - at_line_start = true; - pos += 1; - continue; - } - one_blank_kept = false; - if at_line_start && number != NumberingMode::NumberNone { - (write!(&mut writer, "{0:6}\t", line_counter)).unwrap(); - line_counter += 1; - } - - // print to end of line or end of buffer - let offset = if show_nonprint { - write_nonprint_to_end(&in_buf[pos..], &mut writer, tab) - } else if show_tabs { - write_tab_to_end(&in_buf[pos..], &mut writer) - } else { - write_to_end(&in_buf[pos..], &mut writer) - }; - // end of buffer? - if offset == 0 { - at_line_start = false; - break; - } - // print suitable end of line - writer.write_all(end_of_line).unwrap(); - if interactive { - writer.flush().unwrap(); - } - at_line_start = true; - pos += offset; + for file in files { + let written = write_file_lines(&file[..], options, line_counter); + line_counter += match written { + Ok(lines) => lines, + Err(error) => { + writeln!(&mut stderr(), "{}", error).context(&file[..])?; + error_count += 1; + 0 } } } + + match error_count { + 0 => Ok(()), + _ => Err(CatError::EncounteredErrors(error_count)), + } +} + +/// Outputs file and returns result with the number of lines to stdout +/// from `file`. If line numbering is enabled, then start output +/// numbering at `line_number`. +/// +/// # Arguments +/// +/// * `files` - There is no short circuit when encountiner an error +/// reading a file in this vector +fn write_file_lines(file: &str, options: &OutputOptions, line_number: usize) -> CatResult { + let mut handle = open(&file[..])?; + let mut in_buf = [0; 1024 * 31]; + let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); + let mut at_line_start = true; + let mut one_blank_kept = false; + + let mut lines = 0; + + while let Ok(n) = handle.reader.read(&mut in_buf) { + if n == 0 { + break; + } + let in_buf = &in_buf[..n]; + let mut pos = 0; + while pos < n { + // skip empty line_number enumerating them if needed + if in_buf[pos] == '\n' as u8 { + if !at_line_start || ! options.squeeze_blank || !one_blank_kept { + one_blank_kept = true; + if at_line_start && options.number == NumberingMode::NumberAll { + write!(&mut writer, "{0:6}\t", line_number + lines)?; + lines += 1; + } + writer.write_all(options.end_of_line.as_bytes())?; + if handle.is_interactive { + writer.flush().context(&file[..])?; + } + } + at_line_start = true; + pos += 1; + continue; + } + one_blank_kept = false; + if at_line_start && options.number != NumberingMode::NumberNone { + write!(&mut writer, "{0:6}\t", line_number + lines)?; + lines += 1; + } + + // print to end of line or end of buffer + let offset = if options.show_nonprint { + write_nonprint_to_end(&in_buf[pos..], &mut writer, options.tab.as_bytes()) + } else if options.show_tabs { + write_tab_to_end(&in_buf[pos..], &mut writer) + } else { + write_to_end(&in_buf[pos..], &mut writer) + }; + // end of buffer? + if offset == 0 { + at_line_start = false; + break; + } + // print suitable end of line + writer.write_all(options.end_of_line.as_bytes())?; + if handle.is_interactive { + writer.flush()?; + } + at_line_start = true; + pos += offset; + } + } + + Ok(lines) } // write***_to_end methods diff --git a/tests/test_cat.rs b/tests/test_cat.rs index 6394356bc..c476bb2c0 100644 --- a/tests/test_cat.rs +++ b/tests/test_cat.rs @@ -1,5 +1,8 @@ -use common::util::*; +#[cfg(unix)] +extern crate unix_socket; +extern crate tempdir; +use common::util::*; #[test] fn test_output_multi_files_print_all_chars() { @@ -113,7 +116,7 @@ fn test_non_blank_overrides_number() { .pipe_in("\na\nb\n\n\nc") .succeeds() .stdout_only("\n 1\ta\n 2\tb\n\n\n 3\tc"); - } + } } #[test] @@ -126,3 +129,30 @@ fn test_squeeze_blank_before_numbering() { .stdout_only(" 1\ta\n 2\t\n 3\tb"); } } + + + +#[test] +#[cfg(foo)] +fn test_domain_socket() { + use std::thread; + use self::unix_socket::UnixListener; + use self::tempdir::TempDir; + use std::io::prelude::*; + + let dir = TempDir::new("unix_socket").expect("failed to create dir"); + let socket_path = dir.path().join("sock"); + let listener = UnixListener::bind(&socket_path).expect("failed to create socket"); + + let thread = thread::spawn(move || { + let mut stream = listener.accept().expect("failed to accept connection").0; + stream.write_all(b"a\tb").expect("failed to write test data"); + }); + + new_ucmd!() + .args(&[socket_path]) + .succeeds() + .stdout_only("a\tb"); + + thread.join().unwrap(); +}