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)),
+    }
+}