use std::ffi::OsStr;
use std::fs::File;
use std::io::{self, BufReader, Read, Write};
use std::path::Path;
use crate::checksum::{
AlgoKind, ChecksumError, ReadingMode, SizedAlgoKind, digest_reader, escape_filename,
};
use crate::error::{FromIo, UResult, USimpleError};
use crate::line_ending::LineEnding;
use crate::sum::DigestOutput;
use crate::{show, translate};
const READ_BUFFER_SIZE: usize = 32 * 1024;
pub struct ChecksumComputeOptions {
pub algo_kind: SizedAlgoKind,
pub output_format: OutputFormat,
pub line_ending: LineEnding,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DigestFormat {
Hexadecimal,
Base64,
}
impl DigestFormat {
#[inline]
fn is_base64(self) -> bool {
self == Self::Base64
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum OutputFormat {
Raw,
Legacy,
Tagged(DigestFormat),
Untagged(DigestFormat, ReadingMode),
}
impl OutputFormat {
#[inline]
fn is_raw(&self) -> bool {
*self == Self::Raw
}
pub fn from_cksum(algo: AlgoKind, tag: bool, binary: bool, raw: bool, base64: bool) -> Self {
if raw {
return Self::Raw;
}
if algo.is_legacy() {
return Self::Legacy;
}
let digest_format = if base64 {
DigestFormat::Base64
} else {
DigestFormat::Hexadecimal
};
if tag {
Self::Tagged(digest_format)
} else {
let reading_mode = if binary {
ReadingMode::Binary
} else {
ReadingMode::Text
};
Self::Untagged(digest_format, reading_mode)
}
}
pub fn from_standalone(text: bool, tag: bool) -> Self {
if tag {
Self::Tagged(DigestFormat::Hexadecimal)
} else {
Self::Untagged(
DigestFormat::Hexadecimal,
if text {
ReadingMode::Text
} else {
ReadingMode::Binary
},
)
}
}
}
fn print_legacy_checksum(
options: &ChecksumComputeOptions,
filename: &OsStr,
sum: &DigestOutput,
size: usize,
) {
debug_assert!(options.algo_kind.is_legacy());
debug_assert!(matches!(sum, DigestOutput::U16(_) | DigestOutput::Crc(_)));
let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul {
(filename.to_string_lossy().to_string(), "")
} else {
escape_filename(filename)
};
match (options.algo_kind, sum) {
(SizedAlgoKind::Sysv, DigestOutput::U16(sum)) => print!(
"{prefix}{sum} {}",
size.div_ceil(options.algo_kind.bitlen()),
),
(SizedAlgoKind::Bsd, DigestOutput::U16(sum)) => {
let bsd_width = 5;
print!(
"{prefix}{sum:0bsd_width$} {:bsd_width$}",
size.div_ceil(options.algo_kind.bitlen()),
);
}
(SizedAlgoKind::Crc | SizedAlgoKind::Crc32b, DigestOutput::Crc(sum)) => {
print!("{prefix}{sum} {size}");
}
(algo, output) => unreachable!("Bug: Invalid legacy checksum ({algo:?}, {output:?})"),
}
if escaped_filename != "-" {
print!(" ");
let _dropped_result = io::stdout().write_all(escaped_filename.as_bytes());
}
}
fn print_tagged_checksum(options: &ChecksumComputeOptions, filename: &OsStr, sum: &String) {
let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul {
(filename.to_string_lossy().to_string(), "")
} else {
escape_filename(filename)
};
print!("{prefix}{} (", options.algo_kind.to_tag());
let _dropped_result = io::stdout().write_all(escaped_filename.as_bytes());
print!(") = {sum}");
}
fn print_untagged_checksum(
options: &ChecksumComputeOptions,
filename: &OsStr,
sum: &String,
reading_mode: ReadingMode,
) {
let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul {
(filename.to_string_lossy().to_string(), "")
} else {
escape_filename(filename)
};
print!("{prefix}{sum} {}", reading_mode.as_char());
let _dropped_result = io::stdout().write_all(escaped_filename.as_bytes());
}
pub fn perform_checksum_computation<'a, I>(options: ChecksumComputeOptions, files: I) -> UResult<()>
where
I: Iterator<Item = &'a OsStr>,
{
let mut files = files.peekable();
while let Some(filename) = files.next() {
if options.output_format.is_raw() && files.peek().is_some() {
return Err(Box::new(ChecksumError::RawMultipleFiles));
}
let filepath = Path::new(filename);
let stdin_buf;
let file_buf;
if filepath.is_dir() {
show!(USimpleError::new(
1,
translate!("error-is-a-directory", "file" => filepath.display())
));
continue;
}
let mut file = BufReader::with_capacity(
READ_BUFFER_SIZE,
if filename == "-" {
stdin_buf = io::stdin();
Box::new(stdin_buf) as Box<dyn Read>
} else {
file_buf = match File::open(filepath) {
Ok(file) => file,
Err(err) => {
show!(err.map_err_context(|| filepath.to_string_lossy().into()));
continue;
}
};
Box::new(file_buf) as Box<dyn Read>
},
);
let mut digest = options.algo_kind.create_digest();
let (digest_output, sz) = digest_reader(&mut digest, &mut file, ReadingMode::Binary)
.map_err_context(|| translate!("checksum-error-failed-to-read-input"))?;
let encode_sum = |sum: DigestOutput, df: DigestFormat| {
if df.is_base64() {
sum.to_base64()
} else {
sum.to_hex()
}
};
match options.output_format {
OutputFormat::Raw => {
digest_output.write_raw(io::stdout())?;
return Ok(());
}
OutputFormat::Legacy => {
print_legacy_checksum(&options, filename, &digest_output, sz);
}
OutputFormat::Tagged(digest_format) => {
print_tagged_checksum(
&options,
filename,
&encode_sum(digest_output, digest_format)?,
);
}
OutputFormat::Untagged(digest_format, reading_mode) => {
print_untagged_checksum(
&options,
filename,
&encode_sum(digest_output, digest_format)?,
reading_mode,
);
}
}
print!("{}", options.line_ending);
}
Ok(())
}