use std::fs::{File, OpenOptions};
use std::io::{self, BufReader, Read, Write};
use std::path::Path;
use std::sync::atomic::Ordering;
use crate::io::prefs::{DISPLAY_LEVEL, LZ4IO_SKIPPABLE0, LZ4IO_SKIPPABLEMASK};
use crate::util::is_directory;
pub const STDIN_MARK: &str = "stdin";
pub const STDOUT_MARK: &str = "stdout";
#[cfg(windows)]
pub const NUL_MARK: &str = "nul";
#[cfg(not(windows))]
pub const NUL_MARK: &str = "/dev/null";
pub const NULL_OUTPUT: &str = "null";
#[inline]
fn is_dev_null(s: &str) -> bool {
s == NUL_MARK
}
#[inline]
fn is_stdin(s: &str) -> bool {
s == STDIN_MARK
}
#[inline]
fn is_stdout(s: &str) -> bool {
s == STDOUT_MARK
}
#[inline]
pub fn is_skippable_magic_number(magic: u32) -> bool {
(magic & LZ4IO_SKIPPABLEMASK) == LZ4IO_SKIPPABLE0
}
pub fn open_src_file(path: &str) -> io::Result<Box<dyn Read>> {
if is_stdin(path) {
if DISPLAY_LEVEL.load(Ordering::Relaxed) >= 4 {
eprintln!("Using stdin for input");
}
#[cfg(windows)]
unsafe {
libc::_setmode(0, libc::O_BINARY);
}
return Ok(Box::new(io::stdin()));
}
if is_directory(Path::new(path)) {
if DISPLAY_LEVEL.load(Ordering::Relaxed) >= 1 {
eprintln!("lz4: {} is a directory -- ignored", path);
}
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{}: is a directory", path),
));
}
let f = File::open(path).map_err(|e| {
if DISPLAY_LEVEL.load(Ordering::Relaxed) >= 1 {
eprintln!("{}: {}", path, e);
}
e
})?;
Ok(Box::new(BufReader::new(f)))
}
pub struct DstFile {
inner: Box<dyn Write>,
pub is_stdout: bool,
pub sparse_mode: bool,
}
impl Write for DstFile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
pub fn open_dst_file(path: &str, prefs: &crate::io::prefs::Prefs) -> io::Result<DstFile> {
if is_stdout(path) {
if DISPLAY_LEVEL.load(Ordering::Relaxed) >= 4 {
eprintln!("Using stdout for output");
}
#[cfg(windows)]
unsafe {
libc::_setmode(1, libc::O_BINARY);
}
if prefs.sparse_file_support == 1 && DISPLAY_LEVEL.load(Ordering::Relaxed) >= 4 {
eprintln!(
"Sparse File Support automatically disabled on stdout; \
to force-enable it, add --sparse command"
);
}
return Ok(DstFile {
inner: Box::new(io::stdout()),
is_stdout: true,
sparse_mode: false,
});
}
if is_dev_null(path) {
return Ok(DstFile {
inner: Box::new(io::sink()),
is_stdout: false,
sparse_mode: false,
});
}
if !prefs.overwrite && Path::new(path).exists() {
let display_level = DISPLAY_LEVEL.load(Ordering::Relaxed);
if display_level <= 1 {
eprintln!("{} already exists; not overwritten ", path);
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("{}: already exists; not overwritten", path),
));
}
eprint!("{} already exists; do you want to overwrite (y/N) ? ", path);
let _ = io::stderr().flush();
let mut line = String::new();
io::stdin().read_line(&mut line)?;
let first = line.trim_start().chars().next().unwrap_or('\0');
if first != 'y' && first != 'Y' {
eprintln!(" not overwritten ");
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("{}: not overwritten", path),
));
}
}
let f = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(|e| {
if DISPLAY_LEVEL.load(Ordering::Relaxed) >= 1 {
eprintln!("{}: {}", path, e);
}
e
})?;
let sparse_mode = prefs.sparse_file_support > 0;
#[cfg(windows)]
if sparse_mode {
use std::os::windows::io::AsRawHandle;
unsafe {
let mut bytes_returned: winapi::shared::minwindef::DWORD = 0;
winapi::um::ioapiset::DeviceIoControl(
f.as_raw_handle() as winapi::um::winnt::HANDLE,
winapi::um::winioctl::FSCTL_SET_SPARSE,
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
0,
&mut bytes_returned,
std::ptr::null_mut(),
);
}
}
Ok(DstFile {
inner: Box::new(f),
is_stdout: false,
sparse_mode,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::prefs::Prefs;
#[test]
fn is_skippable_magic_number_range() {
for v in 0x184D2A50u32..=0x184D2A5Fu32 {
assert!(
is_skippable_magic_number(v),
"expected skippable: {:#010x}",
v
);
}
assert!(!is_skippable_magic_number(0x184D2A4F));
assert!(!is_skippable_magic_number(0x184D2A60));
assert!(!is_skippable_magic_number(0x184D2204)); assert!(!is_skippable_magic_number(0x184C2102)); }
#[test]
fn open_src_file_nonexistent_returns_err() {
let result = open_src_file("/nonexistent/path/that/cannot/exist.lz4");
assert!(result.is_err());
}
#[test]
fn open_dst_file_stdout_sentinel() {
let prefs = Prefs::default();
let dst = open_dst_file(STDOUT_MARK, &prefs).unwrap();
assert!(dst.is_stdout);
assert!(!dst.sparse_mode);
}
#[test]
fn open_dst_file_devnull_sentinel() {
let prefs = Prefs::default();
let result = open_dst_file(NUL_MARK, &prefs);
assert!(result.is_ok());
let dst = result.unwrap();
assert!(!dst.is_stdout);
assert!(!dst.sparse_mode);
}
#[test]
fn open_dst_file_null_output_not_sentinel() {
let prefs = Prefs::default();
let result = open_dst_file(NULL_OUTPUT, &prefs);
if let Ok(ref dst) = result {
assert!(!dst.is_stdout);
}
let _ = std::fs::remove_file(NULL_OUTPUT);
}
#[test]
fn open_dst_file_overwrite_false_nonexistent_ok() {
let mut prefs = Prefs::default();
prefs.overwrite = false;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("output.lz4");
let result = open_dst_file(path.to_str().unwrap(), &prefs);
assert!(result.is_ok());
}
#[test]
fn open_dst_file_sparse_mode_reflects_prefs() {
let mut prefs = Prefs::default();
prefs.sparse_file_support = 1;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("sparse.lz4");
let dst = open_dst_file(path.to_str().unwrap(), &prefs).unwrap();
assert!(dst.sparse_mode);
prefs.sparse_file_support = 0;
let path2 = dir.path().join("nosparse.lz4");
let dst2 = open_dst_file(path2.to_str().unwrap(), &prefs).unwrap();
assert!(!dst2.sparse_mode);
}
#[test]
fn open_dst_file_overwrite_false_existing_err() {
use std::sync::atomic::Ordering;
crate::io::prefs::DISPLAY_LEVEL.store(0, Ordering::Relaxed);
let mut prefs = Prefs::default();
prefs.overwrite = false;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("output.lz4");
std::fs::write(&path, b"existing").unwrap();
let result = open_dst_file(path.to_str().unwrap(), &prefs);
assert!(result.is_err());
}
#[test]
fn sentinel_constants() {
assert_eq!(STDIN_MARK, "stdin");
assert_eq!(STDOUT_MARK, "stdout");
#[cfg(not(windows))]
assert_eq!(NUL_MARK, "/dev/null");
#[cfg(windows)]
assert_eq!(NUL_MARK, "nul");
}
}