diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3e63618..beec8b25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@v1.10.1 with: toolchain: ${{ matrix.toolchain.version }} + target: thumbv6m-none-eabi - name: Install just, nextest uses: taiki-e/install-action@v2.44.25 @@ -55,3 +56,7 @@ jobs: - name: Test run: just test + + - name: Build (no-std) + if: matrix.toolchain.name == 'stable' + run: just build-no-std diff --git a/Cargo.toml b/Cargo.toml index 9159040f..90677b5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,14 +8,15 @@ authors = [ "Rob Ede ", ] keywords = ["byte", "byte-size", "utility", "human-readable", "format"] -categories = ["development-tools", "filesystem"] +categories = ["development-tools", "filesystem", "no-std"] repository = "https://github.com/bytesize-rs/bytesize" license = "Apache-2.0" edition = "2021" rust-version = "1.70" [features] -default = [] +default = ["std"] +std = [] arbitrary = ["dep:arbitrary"] serde = ["dep:serde"] diff --git a/ensure-no-std/Cargo.lock b/ensure-no-std/Cargo.lock new file mode 100644 index 00000000..76d3ca0d --- /dev/null +++ b/ensure-no-std/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bytesize" +version = "1.3.0" + +[[package]] +name = "ensure-no-std" +version = "0.1.0" +dependencies = [ + "bytesize", +] diff --git a/ensure-no-std/Cargo.toml b/ensure-no-std/Cargo.toml new file mode 100644 index 00000000..f756489b --- /dev/null +++ b/ensure-no-std/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ensure-no-std" +version = "0.1.0" +publish = false +edition = "2018" + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +bytesize = { path = "..", default-features = false } diff --git a/ensure-no-std/src/compat_test.rs b/ensure-no-std/src/compat_test.rs new file mode 100644 index 00000000..f9135a8c --- /dev/null +++ b/ensure-no-std/src/compat_test.rs @@ -0,0 +1,7 @@ +use alloc::string::ToString as _; + +use bytesize::ByteSize; + +pub fn create_byte_size() { + ByteSize::kib(44).to_string(); +} diff --git a/ensure-no-std/src/main.rs b/ensure-no-std/src/main.rs new file mode 100644 index 00000000..cfde30b0 --- /dev/null +++ b/ensure-no-std/src/main.rs @@ -0,0 +1,36 @@ +#![no_std] +#![no_main] +#![allow(dead_code, clippy::from_over_into)] + +extern crate alloc; + +use alloc::alloc::{GlobalAlloc, Layout}; + +struct NoopAllocator; + +#[global_allocator] +static ALLOCATOR: NoopAllocator = NoopAllocator; + +unsafe impl Sync for NoopAllocator {} + +unsafe impl GlobalAlloc for NoopAllocator { + unsafe fn alloc(&self, _layout: Layout) -> *mut u8 { + unimplemented!() + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { + unimplemented!() + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} + +#[no_mangle] +pub extern "C" fn _start() -> ! { + loop {} +} + +mod compat_test; diff --git a/justfile b/justfile index 9f8cc674..3f1ffb31 100644 --- a/justfile +++ b/justfile @@ -66,6 +66,10 @@ test-coverage-codecov toolchain="": test-coverage-lcov toolchain="": cargo {{ toolchain }} llvm-cov --workspace --all-features --lcov --output-path lcov.info +# Build crate for a no-std target. +build-no-std: + cargo build --target=thumbv6m-none-eabi --manifest-path=./ensure-no-std/Cargo.toml + # Document crates in workspace. [group("docs")] doc *args: diff --git a/src/display.rs b/src/display.rs index 53c8726c..e140db71 100644 --- a/src/display.rs +++ b/src/display.rs @@ -122,6 +122,7 @@ impl fmt::Display for Display { let bytes = self.byte_size.as_u64(); let unit = self.format.unit(); + #[allow(unused_variables)] // used in std contexts let unit_base = self.format.unit_base(); let unit_prefixes = self.format.unit_prefixes(); @@ -132,10 +133,12 @@ impl fmt::Display for Display { write!(f, "{bytes}{unit_separator}B")?; } else { let size = bytes as f64; - let exp = match (size.ln() / unit_base) as usize { - 0 => 1, - e => e, - }; + + #[cfg(feature = "std")] + let exp = ideal_unit_std(size, unit_base); + + #[cfg(not(feature = "std"))] + let exp = ideal_unit_no_std(size, unit); let unit_prefix = unit_prefixes[exp - 1] as char; @@ -150,10 +153,67 @@ impl fmt::Display for Display { } } +#[allow(dead_code)] // used in no-std contexts +fn ideal_unit_no_std(size: f64, unit: u64) -> usize { + assert!(size >= unit as f64, "only called when bytes >= unit"); + + let mut ideal_prefix = 0; + let mut ideal_size = size; + + loop { + ideal_prefix += 1; + ideal_size /= unit as f64; + + if ideal_size < unit as f64 { + break; + } + } + + ideal_prefix +} + +#[cfg(feature = "std")] +#[allow(dead_code)] // used in std contexts +fn ideal_unit_std(size: f64, unit_base: f64) -> usize { + assert!(size.ln() >= unit_base, "only called when bytes >= unit"); + + match (size.ln() / unit_base) as usize { + 0 => unreachable!(), + e => e, + } +} + #[cfg(test)] mod tests { + use alloc::string::ToString as _; + use super::*; + #[cfg(feature = "std")] + quickcheck::quickcheck! { + #[test] + fn ideal_unit_selection_std_no_std_iec(bytes: ByteSize) -> bool { + if bytes.0 < 1025 { + return true; + } + + let size = bytes.0 as f64; + + ideal_unit_std(size, crate::LN_KIB) == ideal_unit_no_std(size, crate::KIB) + } + + #[test] + fn ideal_unit_selection_std_no_std_si(bytes: ByteSize) -> bool { + if bytes.0 < 1025 { + return true; + } + + let size = bytes.0 as f64; + + ideal_unit_std(size, crate::LN_KB) == ideal_unit_no_std(size, crate::KB) + } + } + #[test] fn to_string_iec() { let display = Display { diff --git a/src/lib.rs b/src/lib.rs index fa4c53cf..b766a571 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,10 +41,12 @@ //! assert_eq!(ByteSize::gb(996), minus); //! ``` -use std::{ - fmt, - ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}, -}; +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::string::ToString as _; +use core::{fmt, ops}; #[cfg(feature = "arbitrary")] mod arbitrary; @@ -251,7 +253,7 @@ impl fmt::Debug for ByteSize { macro_rules! commutative_op { ($t:ty) => { - impl Add for $t { + impl ops::Add for $t { type Output = ByteSize; #[inline(always)] fn add(self, rhs: ByteSize) -> ByteSize { @@ -259,7 +261,7 @@ macro_rules! commutative_op { } } - impl Mul for $t { + impl ops::Mul for $t { type Output = ByteSize; #[inline(always)] fn mul(self, rhs: ByteSize) -> ByteSize { @@ -274,7 +276,7 @@ commutative_op!(u32); commutative_op!(u16); commutative_op!(u8); -impl Add for ByteSize { +impl ops::Add for ByteSize { type Output = ByteSize; #[inline(always)] @@ -283,14 +285,14 @@ impl Add for ByteSize { } } -impl AddAssign for ByteSize { +impl ops::AddAssign for ByteSize { #[inline(always)] fn add_assign(&mut self, rhs: ByteSize) { self.0 += rhs.0 } } -impl Add for ByteSize +impl ops::Add for ByteSize where T: Into, { @@ -301,7 +303,7 @@ where } } -impl AddAssign for ByteSize +impl ops::AddAssign for ByteSize where T: Into, { @@ -311,7 +313,7 @@ where } } -impl Sub for ByteSize { +impl ops::Sub for ByteSize { type Output = ByteSize; #[inline(always)] @@ -320,14 +322,14 @@ impl Sub for ByteSize { } } -impl SubAssign for ByteSize { +impl ops::SubAssign for ByteSize { #[inline(always)] fn sub_assign(&mut self, rhs: ByteSize) { self.0 -= rhs.0 } } -impl Sub for ByteSize +impl ops::Sub for ByteSize where T: Into, { @@ -338,7 +340,7 @@ where } } -impl SubAssign for ByteSize +impl ops::SubAssign for ByteSize where T: Into, { @@ -348,7 +350,7 @@ where } } -impl Mul for ByteSize +impl ops::Mul for ByteSize where T: Into, { @@ -359,7 +361,7 @@ where } } -impl MulAssign for ByteSize +impl ops::MulAssign for ByteSize where T: Into, { @@ -371,6 +373,8 @@ where #[cfg(test)] mod property_tests { + use alloc::string::{String, ToString as _}; + use super::*; impl quickcheck::Arbitrary for ByteSize { @@ -393,15 +397,21 @@ mod property_tests { size.to_string().len() < 11 } - // // currently fails on input like "14.0 EiB" - // fn string_round_trip(size: ByteSize) -> bool { - // size.to_string().parse::().unwrap() == size - // } + fn string_round_trip(size: ByteSize) -> bool { + // currently fails on many inputs above the pebibyte level + if size > ByteSize::pib(1) { + return true; + } + + size.to_string().parse::().unwrap() == size + } } } #[cfg(test)] mod tests { + use alloc::format; + use super::*; #[test] diff --git a/src/parse.rs b/src/parse.rs index dc38e65b..012aa289 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,6 +1,9 @@ +use alloc::{format, string::String}; +use core::str; + use super::ByteSize; -impl std::str::FromStr for ByteSize { +impl str::FromStr for ByteSize { type Err = String; fn from_str(value: &str) -> Result { @@ -89,7 +92,7 @@ impl Unit { mod impl_ops { use super::Unit; - use std::ops; + use core::ops; impl ops::Add for Unit { type Output = u64; @@ -156,7 +159,7 @@ mod impl_ops { } } -impl std::str::FromStr for Unit { +impl str::FromStr for Unit { type Err = String; fn from_str(unit: &str) -> Result { @@ -181,6 +184,8 @@ impl std::str::FromStr for Unit { #[cfg(test)] mod tests { + use alloc::string::ToString as _; + use super::*; #[test] diff --git a/src/serde.rs b/src/serde.rs index 3cd3a41c..1933898a 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -1,4 +1,5 @@ -use std::fmt; +use alloc::string::{String, ToString as _}; +use core::fmt; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; @@ -59,7 +60,7 @@ impl Serialize for ByteSize { S: Serializer, { if ser.is_human_readable() { - ::serialize(self.to_string().as_str(), ser) + ::serialize(&self.to_string(), ser) } else { self.0.serialize(ser) }