diff --git a/.arcconfig b/.arcconfig new file mode 100644 index 0000000..6ea9502 --- /dev/null +++ b/.arcconfig @@ -0,0 +1,4 @@ +{ + "phabricator.uri": "https://devcentral.nasqueron.org", + "repository.callsign": "SPACESUM" +} diff --git a/.arclint b/.arclint new file mode 100644 index 0000000..9ff90eb --- /dev/null +++ b/.arclint @@ -0,0 +1,24 @@ +{ + "linters": { + "chmod": { + "type": "chmod" + }, + "filename": { + "type": "filename" + }, + "json": { + "type": "json", + "include": [ + "(^\\.arcconfig$)", + "(^\\.arclint$)", + "(\\.json$)" + ] + }, + "merge-conflict": { + "type": "merge-conflict" + }, + "spelling": { + "type": "spelling" + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..37676d5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "space-sum" +version = "0.1.0" +edition = "2021" +authors = [ + "Sébastien Santoro aka Dereckson" +] +license="BSD-2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "^4.4.11", features = ["derive"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dbed94d --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright 2023 Nasqueron + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..6f06e2a --- /dev/null +++ b/src/error.rs @@ -0,0 +1,61 @@ +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; +use std::io::Error as IOError; + +/// The error type for space-sum operations +pub enum SpaceSumError { + /// The error variant for I/O operations + IO(IOError), + + /// The error variant when parsing a value + Parser(String), +} + +impl From<IOError> for SpaceSumError { + fn from(error: IOError) -> Self { + Self::IO(error) + } +} + +impl Debug for SpaceSumError { + fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::IO(error) => std::fmt::Debug::fmt(&error, fmt), + Self::Parser(error) => write!(fmt, "{error}"), + } + } +} + +impl Display for SpaceSumError { + fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::IO(error) => std::fmt::Display::fmt(&error, fmt), + Self::Parser(error) => write!(fmt, "{error}"), + } + } +} + +impl Error for SpaceSumError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::IO(error) => error.source(), + Self::Parser(_) => None, + } + } + + #[allow(deprecated)] + fn description(&self) -> &str { + match self { + Self::IO(error) => error.description(), + Self::Parser(error) => error, + } + } + + #[allow(deprecated)] + fn cause(&self) -> Option<&dyn Error> { + match self { + Self::IO(error) => error.cause(), + Self::Parser(_) => None, + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..89a3038 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,87 @@ +const KB: f64 = 1024.0; +const MB: f64 = 1048576.0; +const GB: f64 = 1073741824.0; +const TB: f64 = 1099511627776.0; + +/// Parses a size expression +/// +/// # Arguments +/// +/// * `expression`: +/// +/// returns: +/// - None if the expression can't be parsed +/// - Some(size in bytes) if it can be parsed +/// +/// # Examples +/// +/// ``` +/// use space_sum::parse_size; +/// +/// let size = parse_size("1.5G").unwrap(); +/// println!("Size in bytes: {size}"); +/// ``` +/// +/// That will print Size in bytes: 1610612736 +pub fn parse_size(expression: &str) -> Option<f64> { + if expression.is_empty() { + return Some(0.0); + } + + // Assumes ZFS expressions like 3.70M + let len = expression.len(); + let (number, unit) = expression + .split_at(len - 1); + + match unit { + "B" => parse_size_as_bytes(number, 0), + "K" => parse_size_as_bytes(number, 1), + "M" => parse_size_as_bytes(number, 2), + "G" => parse_size_as_bytes(number, 3), + "T" => parse_size_as_bytes(number, 4), + _ => None, + } +} + +fn parse_size_as_bytes(expression: &str, power: i32) -> Option<f64> { + let size: f64 = expression.parse().ok()?; + + Some(size * 1024f64.powi(power)) +} + +/// Print a size using B, K, M, G or T unit appropriately. +/// +/// The size is rounded to three decimals. +/// +/// # Arguments +/// +/// * `size`: The space size, expressed in bytes +/// +/// returns: A expression <size><unit> +/// +/// # Examples +/// +/// ``` +/// use space_sum::human_readable_size; +/// +/// let total_size = 1297991761.9200003; +/// let human_size = human_readable_size(total_size); +/// println!("Total size: {human_size}"); +/// ``` +/// +/// That will print Total size: 1.209G +pub fn human_readable_size(size: f64) -> String { + let scale = size.log(1024.0); + + if scale < 1.0 { + format!("{}B", size) + } else if scale < 2.0 { + format!("{:.3}K", size / KB) + } else if scale < 3.0 { + format!("{:.3}M", size / MB) + } else if scale < 4.0 { + format!("{:.3}G", size / GB) + } else { + format!("{:.3}T", size / TB) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..821f20c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,71 @@ +use std::fs::File; +use std::io::{BufRead, BufReader, stdin}; +use std::io::Error as IOError; +use std::process::exit; + +use clap::Parser; + +use space_sum::*; +use crate::error::SpaceSumError; + +mod error; + +#[derive(Parser, Debug)] +#[command(author, version, about = "Read size expressions like 200M or 2G and compute the sum.", long_about = None)] +struct Args { + #[arg(help = "The file containing sizes to sum. If omitted, values are read from stdin.")] + filename: Option<String>, +} + +fn main() { + let args = Args::parse(); + + let sum = match args.filename { + None => sum_from_stdin(), + Some(filename) => sum_file(&filename), + }; + + match sum { + Ok(sum) => { + let size = human_readable_size(sum); + println!("{size}"); + } + + Err(error) => { + eprintln!("{}", error); + exit(1); + } + } +} + +fn sum_file(filename: &str) -> Result<f64, SpaceSumError> { + let fd = File::open(filename)?; + let buffer = BufReader::new(fd); + + sum_from_lines(Box::new(buffer)) +} + +fn sum_from_stdin() -> Result<f64, SpaceSumError> { + sum_from_lines(Box::new(stdin().lock())) +} + +fn sum_from_lines(buffer: Box<dyn BufRead>) -> Result<f64, SpaceSumError> { + buffer + .lines() + .map(|line| parse_size_line(line)) + .sum() +} + +fn parse_size_line (line: Result<String, IOError>) -> Result<f64, SpaceSumError> { + match line { + Ok(expression) => match parse_size(&expression) { + None => { + let error = format!("Can't parse size expression: {expression}"); + Err(SpaceSumError::Parser(error)) + }, + Some(size) => Ok(size), + }, + + Err(error) => Err(SpaceSumError::IO(error)), + } +}