use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};
use std::path::Path;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
use is_terminal::IsTerminal;
use zeroize::Zeroize;
use crate::{fl, util::LINE_ENDING, wfl, wlnfl};
const SHORT_OUTPUT_LENGTH: usize = 20 * 80;
#[derive(Debug)]
enum FileError {
DenyBinaryOutput,
DenyOverwriteFile(String),
DetectedBinaryOutput,
InvalidFilename(String),
MissingDirectory(String),
}
impl fmt::Display for FileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DenyBinaryOutput => {
wlnfl!(f, "err-deny-binary-output")?;
wfl!(f, "rec-deny-binary-output")
}
Self::DenyOverwriteFile(filename) => {
wfl!(f, "err-deny-overwrite-file", filename = filename.as_str())
}
Self::DetectedBinaryOutput => {
wlnfl!(f, "err-detected-binary")?;
wfl!(f, "rec-detected-binary")
}
Self::InvalidFilename(filename) => {
wfl!(f, "err-invalid-filename", filename = filename.as_str())
}
Self::MissingDirectory(path) => wfl!(f, "err-missing-directory", path = path.as_str()),
}
}
}
impl std::error::Error for FileError {}
pub struct FileReader {
inner: File,
filename: String,
}
pub enum InputReader {
File(FileReader),
Stdin(io::Stdin),
}
impl InputReader {
pub fn new(input: Option<String>) -> io::Result<Self> {
if let Some(filename) = input {
if filename != "-" {
return Ok(InputReader::File(FileReader {
inner: File::open(&filename)?,
filename,
}));
}
}
Ok(InputReader::Stdin(io::stdin()))
}
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Stdin(_)) && io::stdin().is_terminal()
}
pub(crate) fn filename(&self) -> Option<&str> {
if let Self::File(f) = self {
Some(&f.filename)
} else {
None
}
}
}
impl Read for InputReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
InputReader::File(f) => f.inner.read(buf),
InputReader::Stdin(handle) => handle.read(buf),
}
}
}
#[derive(Debug)]
enum StdoutBuffer {
Direct(io::Stdout),
Buffered(Vec<u8>),
}
impl StdoutBuffer {
fn direct() -> Self {
Self::Direct(io::stdout())
}
fn buffered() -> Self {
Self::Buffered(Vec::with_capacity(8 * 1024 * 1024))
}
}
impl Write for StdoutBuffer {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
match self {
StdoutBuffer::Direct(w) => w.write(data),
StdoutBuffer::Buffered(buf) => {
if buf.len() + data.len() > buf.capacity() {
let mut new_buf = Vec::with_capacity(std::cmp::max(
buf.capacity() * 2,
buf.capacity() + data.len(),
));
new_buf.extend_from_slice(buf);
buf.zeroize();
*buf = new_buf;
}
buf.extend_from_slice(data);
Ok(data.len())
}
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
StdoutBuffer::Direct(w) => w.flush(),
StdoutBuffer::Buffered(buf) => {
let mut w = io::stdout();
w.write_all(buf)?;
buf.zeroize();
buf.clear();
w.flush()
}
}
}
}
impl Drop for StdoutBuffer {
fn drop(&mut self) {
let _ = self.flush();
}
}
#[derive(Debug)]
pub enum OutputFormat {
Binary,
Text,
Unknown,
}
#[derive(Debug)]
pub struct StdoutWriter {
inner: StdoutBuffer,
count: usize,
format: OutputFormat,
is_tty: bool,
truncated: bool,
}
impl StdoutWriter {
fn new(format: OutputFormat, is_tty: bool, input_is_tty: bool) -> Self {
StdoutWriter {
inner: if input_is_tty && is_tty {
StdoutBuffer::buffered()
} else {
StdoutBuffer::direct()
},
count: 0,
format,
is_tty,
truncated: false,
}
}
}
impl Write for StdoutWriter {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
if self.is_tty {
if let OutputFormat::Unknown = self.format {
if std::str::from_utf8(data).is_err() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
FileError::DetectedBinaryOutput,
));
}
}
let to_write = if let OutputFormat::Binary = self.format {
data.len()
} else {
if self.truncated || self.count == SHORT_OUTPUT_LENGTH {
if !self.truncated {
self.inner.write_all(LINE_ENDING.as_bytes())?;
self.inner.write_all(b"[")?;
self.inner.write_all(fl!("cli-truncated-tty").as_bytes())?;
self.inner.write_all(b"]")?;
self.inner.write_all(LINE_ENDING.as_bytes())?;
self.truncated = true;
}
return io::sink().write(data);
}
let mut to_write = SHORT_OUTPUT_LENGTH - self.count;
if to_write > data.len() {
to_write = data.len();
}
to_write
};
let mut ret = self.inner.write(&data[..to_write])?;
self.count += to_write;
if let OutputFormat::Binary = self.format {
} else {
if self.count == SHORT_OUTPUT_LENGTH && data.len() > to_write {
if !self.truncated {
self.inner.write_all(LINE_ENDING.as_bytes())?;
self.inner.write_all(b"[")?;
self.inner.write_all(fl!("cli-truncated-tty").as_bytes())?;
self.inner.write_all(b"]")?;
self.inner.write_all(LINE_ENDING.as_bytes())?;
self.truncated = true;
}
ret += io::sink().write(&data[to_write..])?;
}
}
Ok(ret)
} else {
self.inner.write(data)
}
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
#[derive(Debug)]
pub struct LazyFile {
filename: String,
allow_overwrite: bool,
#[cfg(unix)]
mode: u32,
file: Option<io::Result<File>>,
}
impl LazyFile {
fn get_file(&mut self) -> io::Result<&mut File> {
let filename = &self.filename;
if self.file.is_none() {
let mut options = OpenOptions::new();
options.write(true);
if self.allow_overwrite {
options.create(true).truncate(true);
} else {
options.create_new(true);
}
#[cfg(unix)]
options.mode(self.mode);
self.file = Some(options.open(filename));
}
self.file
.as_mut()
.unwrap()
.as_mut()
.map_err(|e| io::Error::new(e.kind(), format!("Failed to open file '{}'", filename)))
}
}
impl io::Write for LazyFile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.get_file()?.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.get_file()?.flush()
}
}
#[derive(Debug)]
pub enum OutputWriter {
File(LazyFile),
Stdout(StdoutWriter),
}
impl OutputWriter {
pub fn new(
output: Option<String>,
allow_overwrite: bool,
mut format: OutputFormat,
_mode: u32,
input_is_tty: bool,
) -> io::Result<Self> {
let is_tty = console::user_attended();
if let Some(filename) = output {
if filename != "-" {
let file_path = Path::new(&filename);
if let Some(dir_path) = file_path.parent() {
if !(dir_path == Path::new("") || dir_path.exists()) {
return Err(io::Error::new(
io::ErrorKind::NotFound,
FileError::MissingDirectory(dir_path.display().to_string()),
));
}
} else {
return Err(io::Error::new(
io::ErrorKind::NotFound,
FileError::InvalidFilename(filename),
));
}
if !allow_overwrite && file_path.exists() {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
FileError::DenyOverwriteFile(filename),
));
}
return Ok(OutputWriter::File(LazyFile {
filename,
allow_overwrite,
#[cfg(unix)]
mode: _mode,
file: None,
}));
} else {
format = OutputFormat::Binary;
}
} else if is_tty {
if let OutputFormat::Binary = format {
return Err(io::Error::new(
io::ErrorKind::Other,
FileError::DenyBinaryOutput,
));
}
}
Ok(OutputWriter::Stdout(StdoutWriter::new(
format,
is_tty,
input_is_tty,
)))
}
pub fn is_terminal(&self) -> bool {
match self {
OutputWriter::File(..) => false,
OutputWriter::Stdout(w) => w.is_tty,
}
}
}
impl Write for OutputWriter {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
match self {
OutputWriter::File(f) => f.write(data),
OutputWriter::Stdout(handle) => handle.write(data),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
OutputWriter::File(f) => f.flush(),
OutputWriter::Stdout(handle) => handle.flush(),
}
}
}
#[cfg(test)]
pub(crate) mod tests {
#[cfg(unix)]
use super::{OutputFormat, OutputWriter};
#[cfg(unix)]
use std::io::Write;
#[cfg(unix)]
#[test]
fn lazy_existing_file_allow_overwrite() {
OutputWriter::new(
Some("/dev/null".to_string()),
true,
OutputFormat::Text,
0o600,
false,
)
.unwrap()
.flush()
.unwrap();
}
#[cfg(unix)]
#[test]
fn lazy_existing_file_forbid_overwrite() {
use std::io;
let e = OutputWriter::new(
Some("/dev/null".to_string()),
false,
OutputFormat::Text,
0o600,
false,
)
.unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::AlreadyExists);
}
}