Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,7 @@ jobs:
- name: Build (no-std)
if: matrix.toolchain.name == 'stable'
run: just build-no-std

- name: Build (no-alloc)
if: matrix.toolchain.name == 'stable'
run: just build-no-alloc
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Add no-alloc support, including parsing and formatting.
- Replace the `String` error from `ByteSize::from_str()` with `ByteSizeParseError`.

## 2.7.0

- Remove no-alloc support because it removed `ByteSize::display()` when default features were disabled.
Expand Down
8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ all-features = true

[features]
default = ["std"]
std = []
alloc = []
std = ["alloc"]
arbitrary = ["dep:arbitrary"]
serde = ["dep:serde_core"]
serde = ["alloc", "dep:serde_core"]

[dependencies]
arbitrary = { version = "1", optional = true }
Expand All @@ -41,6 +42,9 @@ toml = "1.1"
name = "display"
harness = false

[[example]]
name = "ls"

[lints.rust]
rust-2018-idioms = { level = "deny" }
future-incompatible = { level = "deny" }
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,21 @@

Features:

- Pre-defined constants for various size units (e.g., B, Kb, Kib, Mb, Mib, Gb, Gib, ... PB).
- Pre-defined constants for various size units (e.g., B, KB, KiB, MB, MiB, ... EB, EiB).
- `ByteSize` type which presents size units convertible to different size units.
- Arithmetic operations for `ByteSize`.
- `FromStr` impl for `ByteSize`, allowing for parsing string size representations like "1.5KiB" and "521TiB".
- Serde support for binary and human-readable deserializers like JSON.

### Feature flags

- `std` (default): Enables the `alloc` feature and standard library optimizations.
- `alloc`: Enables allocator-backed integrations.
- `arbitrary`: Implements `arbitrary::Arbitrary` for [`ByteSize`].
- `serde`: Enables `alloc` and implements serialization and deserialization for [`ByteSize`].

Parsing and formatting are available without default features and do not allocate.

### Examples

Construction using SI or IEC helpers.
Expand Down
14 changes: 14 additions & 0 deletions ensure-no-alloc/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions ensure-no-alloc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "ensure-no-alloc"
version = "0.1.0"
publish = false
edition = "2018"

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

[dependencies]
bytesize = { path = "..", default-features = false }
48 changes: 48 additions & 0 deletions ensure-no-alloc/src/compat_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use core::fmt::{self, Write as _};

use bytesize::{ByteSize, ByteSizeParseError, Unit, UnitParseError, KIB};

pub fn check() {
assert_error::<ByteSizeParseError>();
assert_error::<UnitParseError>();

let size = "44 KiB".parse::<ByteSize>().unwrap();
let bytes = size.as_u64();

assert!(bytes == KIB * 44);
assert!("KiB".parse::<Unit>().unwrap() * 44 == bytes);

let mut output = Buffer::new();
write!(&mut output, "|{size:>13.5}|").unwrap();
assert!(output.as_str() == "| 44.00000 KiB|");
}

fn assert_error<E: core::error::Error>() {}

struct Buffer {
bytes: [u8; 32],
len: usize,
}

impl Buffer {
const fn new() -> Self {
Self {
bytes: [0; 32],
len: 0,
}
}

fn as_str(&self) -> &str {
core::str::from_utf8(&self.bytes[..self.len]).unwrap()
}
}

impl fmt::Write for Buffer {
fn write_str(&mut self, value: &str) -> fmt::Result {
let end = self.len.checked_add(value.len()).ok_or(fmt::Error)?;
let target = self.bytes.get_mut(self.len..end).ok_or(fmt::Error)?;
target.copy_from_slice(value.as_bytes());
self.len = end;
Ok(())
}
}
16 changes: 16 additions & 0 deletions ensure-no-alloc/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![no_std]
#![no_main]
#![allow(dead_code, clippy::from_over_into)]

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}

#[no_mangle]
pub extern "C" fn _start() -> ! {
compat_test::check();
loop {}
}

mod compat_test;
2 changes: 1 addition & 1 deletion ensure-no-std/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ensure-no-std/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ panic = "abort"
panic = "abort"

[dependencies]
bytesize = { path = "..", default-features = false }
bytesize = { path = "..", default-features = false, features = ["alloc"] }
6 changes: 6 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ update-readme:
[group("lint")]
clippy:
cargo clippy --workspace --all-targets --no-default-features
cargo clippy --workspace --all-targets --no-default-features --features alloc
cargo clippy --workspace --all-targets --all-features

# Test workspace.
[group("test")]
test:
cargo {{ toolchain }} nextest run --workspace --no-default-features
cargo {{ toolchain }} nextest run --workspace --no-default-features --features alloc
cargo {{ toolchain }} nextest run --workspace --all-features
cargo {{ toolchain }} test --doc --workspace --all-features
RUSTDOCFLAGS="-D warnings" cargo {{ toolchain }} doc --workspace --no-deps --all-features
Expand Down Expand Up @@ -72,6 +74,10 @@ test-coverage-lcov:
build-no-std:
cargo build --target=thumbv6m-none-eabi --manifest-path=./ensure-no-std/Cargo.toml

# Build crate for a no-alloc target.
build-no-alloc:
cargo build --target=thumbv6m-none-eabi --manifest-path=./ensure-no-alloc/Cargo.toml

# Document crates in workspace.
[group("docs")]
doc *args:
Expand Down
55 changes: 35 additions & 20 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
//! and "521TiB".
//! - Serde support for binary and human-readable deserializers like JSON.
//!
//! # Feature flags
//!
//! - `std` (default): Enables the `alloc` feature and standard library optimizations.
//! - `alloc`: Enables allocator-backed integrations.
//! - `arbitrary`: Implements `arbitrary::Arbitrary` for [`ByteSize`].
//! - `serde`: Enables `alloc` and implements serialization and deserialization for [`ByteSize`].
//!
//! Parsing and formatting are available without default features and do not allocate.
//!
//! # Examples
//!
//! Construction using SI or IEC helpers.
Expand Down Expand Up @@ -45,9 +54,9 @@

#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(any(feature = "serde", test))]
extern crate alloc;

use alloc::string::ToString as _;
use core::{fmt, iter, ops};

#[cfg(feature = "arbitrary")]
Expand All @@ -59,7 +68,7 @@ mod serde;

pub use self::display::Display;
use self::display::Format;
pub use self::parse::{Unit, UnitParseError};
pub use self::parse::{ByteSizeParseError, Unit, UnitParseError};

/// Number of bytes in 1 kilobyte.
pub const KB: u64 = 1_000;
Expand Down Expand Up @@ -338,25 +347,16 @@ impl fmt::Display for ByteSize {
let display = self.display();

if f.width().is_none() {
// allocation-free fast path for when no formatting options are specified
fmt::Display::fmt(&display, f)
} else {
// `display.to_string()` renders at the default precision, and `f.pad`
// reinterprets the formatter's precision as a *maximum* width. Together
// they drop the requested precision and truncate the value mid-unit
// (e.g. `{:>12.5}` rendered "1.86328 GiB" as "1.9 G"). Render with the
// requested precision first, then apply only the width, fill, and align.
let content = match f.precision() {
Some(precision) => alloc::format!("{display:.precision$}"),
None => display.to_string(),
};

let padding = f
.width()
.unwrap_or(0)
.saturating_sub(content.chars().count());
let mut counter = CharCounter::default();
match f.precision() {
Some(precision) => fmt::write(&mut counter, format_args!("{display:.precision$}"))?,
None => fmt::write(&mut counter, format_args!("{display}"))?,
}
let padding = f.width().unwrap_or(0).saturating_sub(counter.count);
if padding == 0 {
return f.write_str(&content);
return fmt::Display::fmt(&display, f);
}

let (left, right) = match f.align() {
Expand All @@ -370,7 +370,7 @@ impl fmt::Display for ByteSize {
for _ in 0..left {
f.write_str(fill)?;
}
f.write_str(&content)?;
fmt::Display::fmt(&display, f)?;
for _ in 0..right {
f.write_str(fill)?;
}
Expand All @@ -379,6 +379,18 @@ impl fmt::Display for ByteSize {
}
}

#[derive(Default)]
struct CharCounter {
count: usize,
}

impl fmt::Write for CharCounter {
fn write_str(&mut self, value: &str) -> fmt::Result {
self.count = self.count.saturating_add(value.chars().count());
Ok(())
}
}

impl fmt::Debug for ByteSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({} bytes)", self, self.0)
Expand Down Expand Up @@ -608,7 +620,10 @@ mod core_tests {

#[cfg(test)]
mod alloc_tests {
use alloc::{format, string::String};
use alloc::{
format,
string::{String, ToString as _},
};

use super::*;

Expand Down
Loading
Loading