From 17fc4c5ed9e9c40a5743462e603f1ecf0a816abf Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Wed, 3 May 2023 22:18:39 -0700 Subject: [PATCH 1/9] misc: test in debug and release modes --- .github/workflows/ci.yaml | 4 ++++ justfile | 14 +++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 415d040..d22b9c5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,6 +51,7 @@ jobs: - name: Build run: | + cargo build --target ${{ matrix.target }} cargo build --release --target ${{ matrix.target }} - name: Clippy @@ -59,5 +60,8 @@ jobs: - name: Tests run: | + cargo test --all-features --target ${{ matrix.target }} + cargo test --all-features --target ${{ matrix.target }} -- --ignored + cargo test --release --all-features --target ${{ matrix.target }} cargo test --release --all-features --target ${{ matrix.target }} -- --ignored diff --git a/justfile b/justfile index f350525..93b1c58 100644 --- a/justfile +++ b/justfile @@ -43,15 +43,6 @@ bench BENCH="": exit 0 -# Check Release! -@check: - # First let's build the Rust bit. - cargo check \ - --release \ - --target x86_64-unknown-linux-gnu \ - --target-dir "{{ cargo_dir }}" - - # Clean Cargo crap. @clean: # Most things go here. @@ -115,6 +106,11 @@ bench BENCH="": # Unit tests! @test: clear + + cargo test \ + --target x86_64-unknown-linux-gnu \ + --target-dir "{{ cargo_dir }}" + cargo test \ --release \ --target x86_64-unknown-linux-gnu \ From 1d557db67a78bec066bfc37d24412fc97b04cdae Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Fri, 12 May 2023 23:26:17 -0700 Subject: [PATCH 2/9] unit: add debug assertions around `unsafe` blocks --- src/nice_elapsed/mod.rs | 6 ++++-- src/nice_int/mod.rs | 1 + src/nice_int/nice_float.rs | 8 ++++++++ src/nice_int/nice_u8.rs | 7 +++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/nice_elapsed/mod.rs b/src/nice_elapsed/mod.rs index b5621d8..4db24d9 100644 --- a/src/nice_elapsed/mod.rs +++ b/src/nice_elapsed/mod.rs @@ -311,6 +311,7 @@ impl NiceElapsed { /// ``` pub fn as_str(&self) -> &str { // Safety: numbers and labels are valid ASCII. + debug_assert!(self.as_bytes().is_ascii(), "Bug: NiceElapsed is not ASCII."); unsafe { std::str::from_utf8_unchecked(self.as_bytes()) } } } @@ -337,8 +338,9 @@ impl NiceElapsed { u8::from(has_m) + u8::from(has_s); - debug_assert!( - 0 < total, + debug_assert_ne!( + total, + 0, "BUG: NiceElapsed::from_parts should always have a part!" ); diff --git a/src/nice_int/mod.rs b/src/nice_int/mod.rs index 6bfd156..97f27c1 100644 --- a/src/nice_int/mod.rs +++ b/src/nice_int/mod.rs @@ -126,6 +126,7 @@ impl NiceWrapper { /// Return the value as a string slice. pub fn as_str(&self) -> &str { // Safety: numbers are valid ASCII. + debug_assert!(std::str::from_utf8(self.as_bytes()).is_ok(), "NiceWrapper is not UTF."); unsafe { std::str::from_utf8_unchecked(self.as_bytes()) } } } diff --git a/src/nice_int/nice_float.rs b/src/nice_int/nice_float.rs index 419822c..179fc26 100644 --- a/src/nice_int/nice_float.rs +++ b/src/nice_int/nice_float.rs @@ -346,6 +346,10 @@ impl NiceFloat { /// assert_eq!(nice.compact_str(), "12,345.67833333"); // Nothing to trim. /// ``` pub fn compact_str(&self) -> &str { + debug_assert!( + std::str::from_utf8(self.compact_bytes()).is_ok(), + "Bug: NiceFloat is not UTF." + ); unsafe { std::str::from_utf8_unchecked(self.compact_bytes()) } } @@ -411,6 +415,10 @@ impl NiceFloat { /// assert_eq!(nice.precise_str(8), "12,345.67800000"); /// ``` pub fn precise_str(&self, precision: usize) -> &str { + debug_assert!( + std::str::from_utf8(self.precise_bytes(precision)).is_ok(), + "Bug: NiceFloat is not UTF." + ); unsafe { std::str::from_utf8_unchecked(self.precise_bytes(precision)) } } } diff --git a/src/nice_int/nice_u8.rs b/src/nice_int/nice_u8.rs index a7bde8e..51831fa 100644 --- a/src/nice_int/nice_u8.rs +++ b/src/nice_int/nice_u8.rs @@ -138,6 +138,7 @@ impl NiceU8 { /// ``` pub fn as_str2(&self) -> &str { // Safety: numbers are valid ASCII. + debug_assert!(self.as_bytes2().is_ascii(), "Bug: NiceU8 is not ASCII."); unsafe { std::str::from_utf8_unchecked(self.as_bytes2()) } } @@ -159,6 +160,12 @@ impl NiceU8 { /// ``` pub const fn as_str3(&self) -> &str { // Safety: numbers are valid ASCII. + debug_assert!( + self.inner[0].is_ascii_digit() && + self.inner[1].is_ascii_digit() && + self.inner[2].is_ascii_digit(), + "Bug: NiceU8 is not ASCII." + ); unsafe { std::str::from_utf8_unchecked(self.as_bytes3()) } } } From 06a18f4a574605ab62cb6cb41d01b7246462e118 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Fri, 12 May 2023 23:30:24 -0700 Subject: [PATCH 3/9] misc: replace most `unsafe` blocks with safe alternatives --- src/lib.rs | 22 +-- src/nice_elapsed/mod.rs | 280 +++++++++++++++++------------------ src/nice_int/mod.rs | 48 ++---- src/nice_int/nice_float.rs | 55 +++---- src/nice_int/nice_percent.rs | 41 +---- src/nice_int/nice_u16.rs | 31 ++-- src/nice_int/nice_u8.rs | 11 +- src/traits/btou.rs | 28 ++-- src/traits/inflect.rs | 13 +- 9 files changed, 227 insertions(+), 302 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2eab370..1c23adb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -112,28 +112,32 @@ static DOUBLE: [[u8; 2]; 100] = [ [57, 48], [57, 49], [57, 50], [57, 51], [57, 52], [57, 53], [57, 54], [57, 55], [57, 56], [57, 57] ]; -#[allow(unsafe_code)] #[inline] -/// # Double Pointer. +/// # Double Digits. /// -/// This produces a pointer to a specific two-digit subslice of `DOUBLE`. +/// Return both digits, ASCII-fied. /// /// ## Panics /// /// This will panic if the number is greater than 99. -pub(crate) fn double_ptr(idx: usize) -> *const u8 { - debug_assert!(idx < 100, "BUG: Invalid index passed to double_ptr."); - unsafe { DOUBLE.get_unchecked(idx).as_ptr() } -} +pub(crate) fn double(idx: usize) -> [u8; 2] { DOUBLE[idx] } -/// # Double Digits. +#[inline] +#[allow(clippy::cast_possible_truncation)] +/// # Triple Digits. /// /// Return both digits, ASCII-fied. /// /// ## Panics /// /// This will panic if the number is greater than 99. -pub(crate) fn double(idx: usize) -> [u8; 2] { DOUBLE[idx] } +pub(crate) fn triple(idx: usize) -> [u8; 3] { + assert!(idx < 1000, "Bug: Triple must be less than 1000."); + let (div, rem) = div_mod(idx, 100); + let a = div as u8 + b'0'; + let [b, c] = DOUBLE[rem]; + [a, b, c] +} diff --git a/src/nice_elapsed/mod.rs b/src/nice_elapsed/mod.rs index 4db24d9..70f0016 100644 --- a/src/nice_elapsed/mod.rs +++ b/src/nice_elapsed/mod.rs @@ -86,7 +86,7 @@ impl Default for NiceElapsed { #[inline] fn default() -> Self { Self { - inner: [0; SIZE], + inner: [b' '; SIZE], len: 0, } } @@ -181,8 +181,7 @@ impl NiceElapsed { /// ``` pub const fn min() -> Self { Self { - // 0 • s e c o n d s - inner: [48, 32, 115, 101, 99, 111, 110, 100, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + inner: *b"0 seconds ", len: 9, } } @@ -201,8 +200,7 @@ impl NiceElapsed { /// ``` pub const fn max() -> Self { Self { - // > 1 • d a y - inner: [62, 49, 32, 100, 97, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + inner: *b">1 day ", len: 6, } } @@ -317,211 +315,209 @@ impl NiceElapsed { } impl NiceElapsed { - #[allow(clippy::cast_possible_truncation)] // We're checking first. - #[allow(clippy::cast_sign_loss)] // Values are unsigned. - #[allow(clippy::similar_names)] // It's that or the names become tedious. - #[allow(unsafe_code)] + #[allow( + clippy::similar_names, + clippy::many_single_char_names, + clippy::cast_possible_truncation, + )] /// # From DHMS.ms. /// /// Build with days, hours, minutes, seconds, and milliseconds (hundredths). fn from_parts(d: u16, h: u8, m: u8, s: u8, ms: u8) -> Self { - let has_d = 0 < d; - let has_h = 0 < h; - let has_m = 0 < m; - let has_ms = 0 < ms; - let has_s = has_ms || 0 < s; - - // How many elements are there? + // Figure out which parts apply. + let has_d = 0 != d; + let has_h = 0 != h; + let has_m = 0 != m; + let has_ms = 0 != ms; + let has_s = has_ms || 0 != s; + + // How many sections are there to write? let total: u8 = u8::from(has_d) + u8::from(has_h) + u8::from(has_m) + u8::from(has_s); - debug_assert_ne!( - total, - 0, - "BUG: NiceElapsed::from_parts should always have a part!" - ); + // This shouldn't hit, but just in case. + if total == 0 { return Self::min(); } - let mut buf = [0_u8; SIZE]; - let mut end = buf.as_mut_ptr(); + let mut inner = [b' '; SIZE]; + let mut len = 0; let mut idx: u8 = 0; // Days. if has_d { idx += 1; - // If this fits within a u8 range, it is much cheaper to cast down. - if d <= 255 { - end = ElapsedKind::Day.write(end, d as u8, idx, total); + // If the days are small, we can handle the digits like normal. + if d < 10 { + inner[0] = d as u8 + b'0'; + len = 1; + } + else if d < 100 { + inner[..2].copy_from_slice(crate::double(d as usize).as_slice()); + len += 2; + } + else if d < 1000 { + inner[..3].copy_from_slice(crate::triple(d as usize).as_slice()); + len += 3; } - // Otherwise we need to invoke NiceU16 to handle commas, etc. + // Otherwise we'll need to leverage NiceU16. else { let tmp = NiceU16::from(d); - let len = tmp.len(); - unsafe { - std::ptr::copy_nonoverlapping( - tmp.as_bytes().as_ptr(), - end, - len - ); - end = end.add(len); - } - end = ElapsedKind::Day.write_label(end, d == 1); + len += tmp.len(); + inner[..len].copy_from_slice(tmp.as_bytes()); } + len += LabelKind::Day.write_to_slice(1 == d, idx, total, &mut inner[len..]); } // Hours. if has_h { idx += 1; - end = ElapsedKind::Hour.write(end, h, idx, total); + len += write_u8_to_slice(h, &mut inner[len..]); + len += LabelKind::Hour.write_to_slice(1 == h, idx, total, &mut inner[len..]); } // Minutes. if has_m { idx += 1; - end = ElapsedKind::Minute.write(end, m, idx, total); + len += write_u8_to_slice(m, &mut inner[len..]); + len += LabelKind::Minute.write_to_slice(1 == m, idx, total, &mut inner[len..]); } - // Seconds and/or milliseconds. + // Seconds. if has_s { idx += 1; - end = write_joiner(end, idx, total); - end = unsafe { write_u8_advance(end, s, false) }; + len += write_u8_to_slice(s, &mut inner[len..]); + // They might need milliseconds before the label. if has_ms { - unsafe { - std::ptr::write(end, b'.'); - end = write_u8_advance(end.add(1), ms, true); - } + let [b, c] = crate::double(ms as usize); + inner[len..len + 3].copy_from_slice(&[b'.', b, c]); + len += 3; } - end = ElapsedKind::Second.write_label(end, s == 1 && ms == 0); + len += LabelKind::Second.write_to_slice(1 == s && ! has_ms, idx, total, &mut inner[len..]); } - // Put it all together! - Self { - inner: buf, - len: unsafe { end.offset_from(buf.as_ptr()) as usize }, - } + Self { inner, len } } } +#[derive(Debug, Clone, Copy)] +/// # Join Style. +/// +/// The labels are written with their joins in one go. These are the different +/// options. +enum JoinKind { + None, + And, + Comma, + CommaAnd, +} + + + #[derive(Debug, Copy, Clone)] -/// # Unit Helpers. +/// # Labels. /// -/// This abstracts some of the verbosity of formatting. -enum ElapsedKind { +/// This holds the different labels/units for each time part. +enum LabelKind { Day, Hour, Minute, Second, } -impl ElapsedKind { - /// # Label. - /// - /// Return the plural label with a leading space. - const fn label(self) -> &'static [u8] { - match self { - Self::Day => b" days", - Self::Hour => b" hours", - Self::Minute => b" minutes", - Self::Second => b" seconds", - } +impl LabelKind { + /// # Write Label to Slice. + fn write_to_slice(self, singular: bool, idx: u8, total: u8, buf: &mut [u8]) -> usize { + let join = + // The last section needs no joiner. + if idx == total { JoinKind::None } + // If there are two sections, this must be the first, and simply + // needs an " and ". + else if total == 2 { JoinKind::And } + // If this is the penultimate section (of more than two), we need + // a comma and an and. + else if idx + 1 == total { JoinKind::CommaAnd } + // Otherwise just a comma. + else { JoinKind::Comma }; + + let new = + if singular { self.as_bytes_singular(join) } + else { self.as_bytes_plural(join) }; + + let len = new.len(); + buf[..len].copy_from_slice(new); + len } - #[allow(unsafe_code)] - /// # Write Joiner, Value, Label. - fn write(self, mut dst: *mut u8, val: u8, idx: u8, total: u8) -> *mut u8 { - dst = write_joiner(dst, idx, total); - dst = unsafe { write_u8_advance(dst, val, false) }; - self.write_label(dst, val == 1) - } + /// # As Bytes (Singular). + const fn as_bytes_singular(self, join: JoinKind) -> &'static [u8] { + match (self, join) { + (Self::Day, JoinKind::And) => b" day and ", + (Self::Day, JoinKind::Comma) => b" day, ", + (Self::Day, _) => b" day", - #[allow(unsafe_code)] - /// # Write Label. - const fn write_label(self, dst: *mut u8, singular: bool) -> *mut u8 { - let label = self.label(); - let len = - if singular { label.len() - 1 } - else { label.len() }; - - unsafe { - std::ptr::copy_nonoverlapping(label.as_ptr(), dst, len); - dst.add(len) + (Self::Hour, JoinKind::None) => b" hour", + (Self::Hour, JoinKind::And) => b" hour and ", + (Self::Hour, JoinKind::Comma) => b" hour, ", + (Self::Hour, JoinKind::CommaAnd) => b" hour, and ", + + (Self::Minute, JoinKind::None) => b" minute", + (Self::Minute, JoinKind::And) => b" minute and ", + (Self::Minute, JoinKind::Comma) => b" minute, ", + (Self::Minute, JoinKind::CommaAnd) => b" minute, and ", + + (Self::Second, _) => b" second", } } -} + /// # As Bytes (Plural). + const fn as_bytes_plural(self, join: JoinKind) -> &'static [u8] { + match (self, join) { + (Self::Day, JoinKind::And) => b" days and ", + (Self::Day, JoinKind::Comma) => b" days, ", + (Self::Day, _) => b" days", + (Self::Hour, JoinKind::None) => b" hours", + (Self::Hour, JoinKind::And) => b" hours and ", + (Self::Hour, JoinKind::Comma) => b" hours, ", + (Self::Hour, JoinKind::CommaAnd) => b" hours, and ", -#[allow(unsafe_code)] -/// # Write u8. -/// -/// This will quickly write a `u8` number as a UTF-8 byte slice to the provided -/// pointer, and return a new pointer advanced to the next position (after -/// however many digits were written). -/// -/// If `two == true`, a leading zero will be printed for single-digit values. -/// In practice, this only applies when writing milliseconds. -/// -/// ## Safety -/// -/// The pointer must have enough space for the value, i.e. 1-2 digits. This -/// isn't a problem in practice given the method calls are all private. -unsafe fn write_u8_advance(buf: *mut u8, num: u8, two: bool) -> *mut u8 { - debug_assert!(num < 100, "BUG: write_u8_advance should always be under 100."); - - // Two digits. - if two || 9 < num { - std::ptr::copy_nonoverlapping(crate::double_ptr(num as usize), buf, 2); - buf.add(2) - } - // One digit. - else { - std::ptr::write(buf, num + b'0'); - buf.add(1) + (Self::Minute, JoinKind::None) => b" minutes", + (Self::Minute, JoinKind::And) => b" minutes and ", + (Self::Minute, JoinKind::Comma) => b" minutes, ", + (Self::Minute, JoinKind::CommaAnd) => b" minutes, and ", + + (Self::Second, _) => b" seconds", + } } } -#[allow(unsafe_code)] -/// # Write Joiner. + + +#[inline] +/// # Write U8. /// -/// This will add commas and/or ands as necessary, based on how many entries -/// there are, and where we're at in that list. -const fn write_joiner(dst: *mut u8, idx: u8, total: u8) -> *mut u8 { - // No joiner ever needed. - if total < 2 || idx < 2 { dst } - // We're at the end. - else if idx == total { - // Two items need a naked "and" between them. - if 2 == total { - unsafe { - std::ptr::copy_nonoverlapping(b" and ".as_ptr(), dst, 5); - dst.add(5) - } - } - // More than two items need a "comma-and" between them. - else { - unsafe { - std::ptr::copy_nonoverlapping(b", and ".as_ptr(), dst, 6); - dst.add(6) - } - } +/// This converts a U8 to ASCII and writes it to the buffer without leading +/// zeroes, returning the length written. +fn write_u8_to_slice(num: u8, slice: &mut [u8]) -> usize { + if 99 < num { + slice[..3].copy_from_slice(crate::triple(num as usize).as_slice()); + 3 } - // We just need a comma. - else if 2 < total { - unsafe { - std::ptr::copy_nonoverlapping(b", ".as_ptr(), dst, 2); - dst.add(2) - } + else if 9 < num { + slice[..2].copy_from_slice(crate::double(num as usize).as_slice()); + 2 + } + else { + slice[0] = num + b'0'; + 1 } - // No joiner needed this time. - else { dst } } diff --git a/src/nice_int/mod.rs b/src/nice_int/mod.rs index 97f27c1..9b0f01f 100644 --- a/src/nice_int/mod.rs +++ b/src/nice_int/mod.rs @@ -175,35 +175,34 @@ macro_rules! nice_parse { } impl $nice { - #[allow(clippy::cast_possible_truncation, unsafe_code)] + #[allow(clippy::cast_possible_truncation)] /// # Parse. fn parse(&mut self, mut num: $uint) { - let ptr = self.inner.as_mut_ptr(); - - while 999 < num { - let (div, rem) = crate::div_mod(num, 1000); - self.from -= 4; - unsafe { super::write_u8_3(ptr.add(self.from + 1), rem as u16); } - num = div; + for chunk in self.inner.rchunks_exact_mut(4) { + if 999 < num { + let (div, rem) = crate::div_mod(num, 1000); + chunk[1..].copy_from_slice(crate::triple(rem as usize).as_slice()); + self.from -= 4; + num = div; + } + else { break; } } if 99 < num { self.from -= 3; - unsafe { super::write_u8_3(ptr.add(self.from), num as u16); } + self.inner[self.from..self.from + 3].copy_from_slice( + crate::triple(num as usize).as_slice() + ); } else if 9 < num { self.from -= 2; - unsafe { - std::ptr::copy_nonoverlapping( - crate::double_ptr(num as usize), - ptr.add(self.from), - 2 - ); - } + self.inner[self.from..self.from + 2].copy_from_slice( + crate::double(num as usize).as_slice() + ); } else { self.from -= 1; - unsafe { std::ptr::write(ptr.add(self.from), num as u8 + b'0'); } + self.inner[self.from] = num as u8 + b'0'; } } } @@ -215,18 +214,3 @@ pub(self) use { nice_from_nz, nice_parse, }; - - - -#[allow(unsafe_code, clippy::cast_possible_truncation)] // One digit always fits u8. -/// # Write `u8` x 3 -/// -/// ## Safety -/// -/// The destination pointer must have at least 3 bytes free or undefined -/// things may happen! -unsafe fn write_u8_3(buf: *mut u8, num: u16) { - let (div, rem) = crate::div_mod(num, 100); - std::ptr::write(buf, div as u8 + b'0'); - std::ptr::copy_nonoverlapping(crate::double_ptr(rem as usize), buf.add(1), 2); -} diff --git a/src/nice_int/nice_float.rs b/src/nice_int/nice_float.rs index 179fc26..d3d1355 100644 --- a/src/nice_int/nice_float.rs +++ b/src/nice_int/nice_float.rs @@ -440,7 +440,6 @@ impl NiceFloat { ) } - #[allow(unsafe_code)] #[allow(clippy::cast_possible_truncation)] // They fit. /// # Parse Top. /// @@ -452,48 +451,42 @@ impl NiceFloat { if 0 != top { // Nudge the pointer to the dot; we'll re-rewind after each write. self.from = IDX_DOT; - let ptr = self.inner.as_mut_ptr(); - while 999 < top { - let (div, rem) = crate::div_mod(top, 1000); - self.from -= 4; - unsafe { super::write_u8_3(ptr.add(self.from + 1), rem as u16); } - top = div; + for chunk in self.inner[..IDX_DOT].rchunks_exact_mut(4) { + if 999 < top { + let (div, rem) = crate::div_mod(top, 1000); + chunk[1..].copy_from_slice(crate::triple(rem as usize).as_slice()); + self.from -= 4; + top = div; + } + else { break; } } if 99 < top { self.from -= 3; - unsafe { super::write_u8_3(ptr.add(self.from), top as u16); } + self.inner[self.from..self.from + 3].copy_from_slice( + crate::triple(top as usize).as_slice() + ); } else if 9 < top { self.from -= 2; - unsafe { - std::ptr::copy_nonoverlapping( - crate::double_ptr(top as usize), - ptr.add(self.from), - 2 - ); - } + self.inner[self.from..self.from + 2].copy_from_slice( + crate::double(top as usize).as_slice() + ); } else { self.from -= 1; - unsafe { std::ptr::write(ptr.add(self.from), top as u8 + b'0'); } + self.inner[self.from] = top as u8 + b'0'; } // Negative? if neg { self.from -= 1; - - // Safety: there are (NiceU64::MAX + 1) spaces behind the dot so - // there will always be room. - unsafe { - std::ptr::write(ptr.add(self.from), b'-'); - } + self.inner[self.from] = b'-'; } } } - #[allow(unsafe_code)] /// # Parse Bottom. /// /// This writes the fractional part of the float, if any. @@ -503,33 +496,21 @@ impl NiceFloat { /// write. fn parse_bottom(&mut self, mut bottom: u32) { if 0 != bottom { - let ptr = self.inner.as_mut_ptr(); let mut divisor = 1_000_000_u32; - let mut idx = IDX_DOT + 1; - for _ in 0..4 { + for chunk in self.inner[IDX_DOT + 1..].chunks_exact_mut(2) { let (a, b) = crate::div_mod(bottom, divisor); // Write the leftmost two digits. if 0 != a { - // Safety: 2 bytes x 4 iterations = 8 bytes, the total - // number of bytes reserved to the right of the dot. - unsafe { - std::ptr::copy_nonoverlapping( - crate::double_ptr(a as usize), - ptr.add(idx), - 2, - ); - } + chunk.copy_from_slice(crate::double(a as usize).as_slice()); } // Quitting time? if 0 == b { break; } - // Ready the next round. bottom = b; divisor /= 100; - idx += 2; } } } diff --git a/src/nice_int/nice_percent.rs b/src/nice_int/nice_percent.rs index a549485..8732931 100644 --- a/src/nice_int/nice_percent.rs +++ b/src/nice_int/nice_percent.rs @@ -9,9 +9,6 @@ use crate::NiceWrapper; /// # Total Buffer Size. const SIZE: usize = 7; -/// # Starting Index For Percentage Decimal. -const IDX_PERCENT_DECIMAL: usize = SIZE - 3; - /// # Zero. const ZERO: [u8; SIZE] = [b'0', b'0', b'0', b'.', b'0', b'0', b'%']; @@ -66,7 +63,6 @@ impl Default for NicePercent { macro_rules! nice_from { ($($float:ty),+ $(,)?) => ($( impl From<$float> for NicePercent { - #[allow(unsafe_code)] fn from(num: $float) -> Self { // Shortcut for overflowing values. if num <= 0.0 || ! num.is_normal() { return Self::min(); } @@ -81,42 +77,17 @@ macro_rules! nice_from { if whole == 0 { return Self::min(); } else if 9999 < whole { return Self::max(); } - // Start with 0.00%. - let mut out = Self::min(); - let ptr = out.inner.as_mut_ptr(); - // Split the top and bottom. let (top, bottom) = crate::div_mod(whole, 100); - // Write the integer part. - if 9 < top { - out.from -= 1; - unsafe { - std::ptr::copy_nonoverlapping( - crate::double_ptr(top as usize), - ptr.add(out.from), - 2 - ); - } - } - else if 0 < top { - unsafe { - std::ptr::write(ptr.add(out.from), top as u8 + b'0'); - } - } + let [a, b] = crate::double(top as usize); + let from = if a == b'0' { SIZE - 5 } else { SIZE - 6 }; + let [c, d] = crate::double(bottom as usize); - // Write the fractional part. - if 0 < bottom { - unsafe { - std::ptr::copy_nonoverlapping( - crate::double_ptr(bottom as usize), - ptr.add(IDX_PERCENT_DECIMAL), - 2 - ); - } + Self { + inner: [b'0', a, b, b'.', c, d, b'%'], + from, } - - out } } )+); diff --git a/src/nice_int/nice_u16.rs b/src/nice_int/nice_u16.rs index 4b50103..a77a3eb 100644 --- a/src/nice_int/nice_u16.rs +++ b/src/nice_int/nice_u16.rs @@ -65,48 +65,39 @@ super::nice_from_nz!(NiceU16, NonZeroU16); impl From for NiceU16 { #[allow(clippy::cast_possible_truncation)] // One digit always fits u8. - #[allow(unsafe_code)] + #[allow(clippy::many_single_char_names)] // ABCDE keeps the ordering straight. fn from(num: u16) -> Self { if 999 < num { - let mut inner = ZERO; - let ptr = inner.as_mut_ptr(); let (num, rem) = crate::div_mod(num, 1000); - unsafe { super::write_u8_3(ptr.add(3), rem); } + let [c, d, e] = crate::triple(rem as usize); if 9 < num { - unsafe { - std::ptr::copy_nonoverlapping( - crate::double_ptr(num as usize), - ptr, - 2 - ); - } + let [a, b] = crate::double(num as usize); Self { - inner, + inner: [a, b, b',', c, d, e], from: 0, } } else { - unsafe { std::ptr::write(ptr.add(1), num as u8 + b'0'); } + let b = num as u8 + b'0'; Self { - inner, + inner: [b'0', b, b',', c, d, e], from: 1, } } } else if 99 < num { - let mut inner = ZERO; - unsafe { super::write_u8_3(inner.as_mut_ptr().add(3), num); } + let [c, d, e] = crate::triple(num as usize); Self { - inner, + inner: [b'0', b'0', b',', c, d, e], from: 3, } } else { - let [a, b] = crate::double(num as usize); + let [d, e] = crate::double(num as usize); Self { - inner: [b'0', b'0', b',', b'0', a, b], - from: if a == b'0' { 5 } else { 4 }, + inner: [b'0', b'0', b',', b'0', d, e], + from: if d == b'0' { 5 } else { 4 }, } } } diff --git a/src/nice_int/nice_u8.rs b/src/nice_int/nice_u8.rs index 51831fa..7fa24d8 100644 --- a/src/nice_int/nice_u8.rs +++ b/src/nice_int/nice_u8.rs @@ -62,21 +62,18 @@ pub type NiceU8 = NiceWrapper; impl From for NiceU8 { #[allow(clippy::cast_lossless)] // Seems less performant. - #[allow(unsafe_code)] fn from(num: u8) -> Self { if 99 < num { - let mut inner = ZERO; - unsafe { super::write_u8_3(inner.as_mut_ptr(), num as u16); } Self { - inner, + inner: crate::triple(num as usize), from: 0, } } else { - let [a, b] = crate::double(num as usize); + let [b, c] = crate::double(num as usize); Self { - inner: [b'0', a, b], - from: if a == b'0' { 2 } else { 1 }, + inner: [b'0', b, c], + from: if b == b'0' { 2 } else { 1 }, } } } diff --git a/src/traits/btou.rs b/src/traits/btou.rs index 5987235..d64481c 100644 --- a/src/traits/btou.rs +++ b/src/traits/btou.rs @@ -419,14 +419,13 @@ const fn parse1(byte: u8) -> Option { } #[cfg(target_endian = "little")] -#[allow(unsafe_code)] /// # Parse Two. /// /// This parses two digits as a single `u16`, reducing the number of /// operations that would otherwise be required. const fn parse2(src: &[u8]) -> Option { - debug_assert!(src.len() == 2); - let chunk = u16::from_le_bytes(unsafe { *(src.as_ptr().cast()) }) ^ 0x3030_u16; + assert!(src.len() == 2, "Bug: parse2 requires 2 bytes."); + let chunk = u16::from_le_bytes([src[0], src[1]]) ^ 0x3030_u16; // Make sure the slice contains only ASCII digits. if (chunk & 0xf0f0_u16) | (chunk.wrapping_add(0x7676_u16) & 0x8080_u16) == 0 { @@ -441,15 +440,16 @@ const fn parse2(src: &[u8]) -> Option { #[cfg(target_endian = "little")] #[allow(clippy::cast_possible_truncation)] // Four digits always fit `u16`. -#[allow(unsafe_code)] /// # Parse Four. /// /// This parses four digits as a single `u32`, reducing the number of /// operations that would otherwise be required. The return value is downcast /// to `u16` because four digits will always fit the type. const fn parse4(src: &[u8]) -> Option { - debug_assert!(src.len() == 4); - let chunk = u32::from_le_bytes(unsafe { *(src.as_ptr().cast()) }) ^ 0x3030_3030; + assert!(src.len() == 4, "Bug: parse4 requires 4 bytes."); + let chunk = u32::from_le_bytes([ + src[0], src[1], src[2], src[3], + ]) ^ 0x3030_3030; // Make sure the slice contains only ASCII digits. if (chunk & 0xf0f0_f0f0_u32) | (chunk.wrapping_add(0x7676_7676_u32) & 0x8080_8080_u32) == 0 { @@ -473,15 +473,16 @@ const fn parse4(src: &[u8]) -> Option { #[cfg(target_endian = "little")] #[allow(clippy::cast_possible_truncation)] // Eight digits always fit `u32`. -#[allow(unsafe_code)] /// # Parse Eight. /// /// This parses eight digits as a single `u64`, reducing the number of /// operations that would otherwise be required. The return value is downcast /// to `u32` because eight digits will always fit the type. const fn parse8(src: &[u8]) -> Option { - debug_assert!(src.len() == 8); - let chunk = u64::from_le_bytes(unsafe { *(src.as_ptr().cast()) }) ^ 0x3030_3030_3030_3030_u64; + assert!(src.len() == 8, "Bug: parse8 requires 8 bytes."); + let chunk = u64::from_le_bytes([ + src[0], src[1], src[2], src[3], src[4], src[5], src[6], src[7], + ]) ^ 0x3030_3030_3030_3030_u64; // Make sure the slice contains only ASCII digits. let chk = chunk.wrapping_add(0x7676_7676_7676_7676_u64); @@ -507,16 +508,17 @@ const fn parse8(src: &[u8]) -> Option { #[cfg(target_endian = "little")] #[allow(clippy::cast_possible_truncation)] // Sixteen digits always fit `u16`. -#[allow(unsafe_code)] /// # Parse Sixteen. /// /// This parses sixteen digits as a single `u128`, reducing the number of /// operations that would otherwise be required. The return value is downcast /// to `u64` because sixteen digits will always fit the type. const fn parse16(src: &[u8]) -> Option { - debug_assert!(src.len() == 16); - - let chunk = u128::from_le_bytes(unsafe { *(src.as_ptr().cast()) }) ^ + assert!(src.len() == 16, "Bug: parse16 requires 16 bytes."); + let chunk = u128::from_le_bytes([ + src[0], src[1], src[2], src[3], src[4], src[5], src[6], src[7], + src[8], src[9], src[10], src[11], src[12], src[13], src[14], src[15], + ]) ^ 0x3030_3030_3030_3030_3030_3030_3030_3030_u128; // Make sure the slice contains only ASCII digits. diff --git a/src/traits/inflect.rs b/src/traits/inflect.rs index 2fd1c34..12af6d9 100644 --- a/src/traits/inflect.rs +++ b/src/traits/inflect.rs @@ -82,7 +82,6 @@ macro_rules! inflect { // Nonzero. ($ty:ty, $one:expr) => ( - #[allow(unsafe_code)] impl Inflection for $ty { fn inflect<'a>(self, singular: &'a str, plural: &'a str) -> &'a str { if self == $one { singular } else { plural } @@ -157,11 +156,11 @@ inflect_nice!(u16, NiceU16, 1); inflect_nice!(u32, NiceU32, 1); inflect_nice!(u64, NiceU64, 1); inflect_nice!(usize, NiceU64, 1); -inflect_nice!(NonZeroU8, NiceU8, unsafe { Self::new_unchecked(1) }); -inflect_nice!(NonZeroU16, NiceU16, unsafe { Self::new_unchecked(1) }); -inflect_nice!(NonZeroU32, NiceU32, unsafe { Self::new_unchecked(1) }); -inflect_nice!(NonZeroU64, NiceU64, unsafe { Self::new_unchecked(1) }); -inflect_nice!(NonZeroUsize, NiceU64, unsafe { Self::new_unchecked(1) }); +inflect_nice!(NonZeroU8, NiceU8, Self::MIN); +inflect_nice!(NonZeroU16, NiceU16, Self::MIN); +inflect_nice!(NonZeroU32, NiceU32, Self::MIN); +inflect_nice!(NonZeroU64, NiceU64, Self::MIN); +inflect_nice!(NonZeroUsize, NiceU64, Self::MIN); inflect_nice!(i8, NiceU8, 1, unsigned_abs); inflect_nice!(i16, NiceU16, 1, unsigned_abs); inflect_nice!(i32, NiceU32, 1, unsigned_abs); @@ -171,7 +170,7 @@ inflect_nice!(isize, NiceU64, 1, unsigned_abs); // These aren't nice, but we can still do basic inflection. inflect!(u128, 1); inflect!(i128, 1, unsigned_abs); -inflect!(NonZeroU128, unsafe { Self::new_unchecked(1) }); +inflect!(NonZeroU128, Self::MIN); impl Inflection for f32 { /// # Inflect a String. From 9964eb66bb0d3672b6ec9dd5c564414c660104dd Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Sun, 14 May 2023 20:12:12 -0700 Subject: [PATCH 4/9] bump: msrv 1.70 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d1c3ec5..15cd875 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "dactyl" version = "0.4.8" authors = ["Blobfolio, LLC. "] edition = "2021" -rust-version = "1.63" +rust-version = "1.70" description = "A small library to quickly stringify integers with basic formatting." license = "WTFPL" repository = "https://github.com/Blobfolio/dactyl" From b0261e45a79a2db80475d05c67dee35adebf1d19 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Mon, 15 May 2023 20:25:52 -0700 Subject: [PATCH 5/9] bump: 0.5.0 --- CHANGELOG.md | 14 ++++++++++++++ Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3268100..9fff511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ +## [0.5.0](https://github.com/Blobfolio/dactyl/releases/tag/v0.5.0) - 2023-06-01 + +### Breaking + +* Bump MSRV to `1.70` + +### Changed + +* Replace (most) `unsafe` blocks w/ safe alternatives +* Add debug assertions around remaining `unsafe` blocks for extra/redundant test coverage +* Run CI build/test in both debug and release modes + + + ## [0.4.8](https://github.com/Blobfolio/dactyl/releases/tag/v0.4.8) - 2023-02-16 ### New diff --git a/Cargo.toml b/Cargo.toml index 15cd875..e3b834a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dactyl" -version = "0.4.8" +version = "0.5.0" authors = ["Blobfolio, LLC. "] edition = "2021" rust-version = "1.70" From df19d667ba5de990751c4fd5f5007be38b22db56 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Mon, 15 May 2023 20:33:33 -0700 Subject: [PATCH 6/9] cleanup: remove deprecated `NiceElapsed::max` --- CHANGELOG.md | 8 +++++--- src/nice_elapsed/mod.rs | 19 ------------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fff511..0448ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,16 +4,18 @@ ## [0.5.0](https://github.com/Blobfolio/dactyl/releases/tag/v0.5.0) - 2023-06-01 -### Breaking - -* Bump MSRV to `1.70` ### Changed +* Bump MSRV to `1.70` * Replace (most) `unsafe` blocks w/ safe alternatives * Add debug assertions around remaining `unsafe` blocks for extra/redundant test coverage * Run CI build/test in both debug and release modes +### Removed + +* `NiceElapsed::max` + ## [0.4.8](https://github.com/Blobfolio/dactyl/releases/tag/v0.4.8) - 2023-02-16 diff --git a/src/nice_elapsed/mod.rs b/src/nice_elapsed/mod.rs index 70f0016..8cf6998 100644 --- a/src/nice_elapsed/mod.rs +++ b/src/nice_elapsed/mod.rs @@ -186,25 +186,6 @@ impl NiceElapsed { } } - #[deprecated(since = "0.4.3", note = "NiceElapsed now supports days; this method is now moot")] - #[must_use] - /// # Maximum Value - /// - /// This returns a value that prints as `>1 day`. - /// - /// ## Examples - /// - /// ``` - /// use dactyl::NiceElapsed; - /// assert_eq!(NiceElapsed::max().as_str(), ">1 day"); - /// ``` - pub const fn max() -> Self { - Self { - inner: *b">1 day ", - len: 6, - } - } - #[allow(clippy::integer_division)] // It's fine. #[must_use] /// # Time Chunks (with Days). From f5fdc81588281735cba54e395d0e9eb948e5449d Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Mon, 15 May 2023 21:59:13 -0700 Subject: [PATCH 7/9] doc: update version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f91004..b0c53b6 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Add `dactyl` to your `dependencies` in `Cargo.toml`, like: ``` [dependencies] -dactyl = "0.4.*" +dactyl = "0.5.*" ``` From 197582997096c8bc67c22c148af8dd0327c87585 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Thu, 18 May 2023 20:06:50 -0700 Subject: [PATCH 8/9] ci: test MSRV --- .github/workflows/ci.yaml | 11 +++----- .github/workflows/msrv.yaml | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/msrv.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d22b9c5..0ca8831 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,9 +2,9 @@ name: Build on: push: - branches: [ master ] + branches: [] pull_request: - branches: [ master ] + branches: [] defaults: run: @@ -60,8 +60,5 @@ jobs: - name: Tests run: | - cargo test --all-features --target ${{ matrix.target }} - cargo test --all-features --target ${{ matrix.target }} -- --ignored - - cargo test --release --all-features --target ${{ matrix.target }} - cargo test --release --all-features --target ${{ matrix.target }} -- --ignored + cargo test --all-features --target ${{ matrix.target }} -- --include-ignored + cargo test --release --all-features --target ${{ matrix.target }} -- --include-ignored diff --git a/.github/workflows/msrv.yaml b/.github/workflows/msrv.yaml new file mode 100644 index 0000000..ab525a6 --- /dev/null +++ b/.github/workflows/msrv.yaml @@ -0,0 +1,56 @@ +name: MSRV + +on: + push: + branches: [] + pull_request: + branches: [] + +defaults: + run: + shell: bash + +env: + CARGO_TERM_COLOR: always + +jobs: + all: + name: All + + strategy: + matrix: + target: + - x86_64-unknown-linux-gnu + - x86_64-apple-darwin + - x86_64-pc-windows-msvc + include: + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-apple-darwin + os: macos-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + + runs-on: ${{matrix.os}} + + env: + RUSTFLAGS: "-D warnings" + + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - uses: taiki-e/install-action@v2 + with: + tool: cargo-msrv + + - name: Info + run: | + rustup --version + cargo --version + cargo clippy --version + + - name: MSRV + run: | + cargo msrv verify From 3486c623af388e7e69c09ccce6bca87249ffded5 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Thu, 1 Jun 2023 09:55:50 -0700 Subject: [PATCH 9/9] docs --- CHANGELOG.md | 5 +++-- CREDITS.md | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0448ea9..5547b28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,9 @@ * Bump MSRV to `1.70` * Replace (most) `unsafe` blocks w/ safe alternatives -* Add debug assertions around remaining `unsafe` blocks for extra/redundant test coverage -* Run CI build/test in both debug and release modes +* Add debug/assertions around remaining `unsafe` blocks for extra/redundant test coverage +* CI: run tests in both debug and release modes +* CI: test MSRV ### Removed diff --git a/CREDITS.md b/CREDITS.md index 671f567..7481c42 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,7 +1,7 @@ # Project Dependencies Package: dactyl - Version: 0.4.8 - Generated: 2023-02-16 19:35:21 UTC + Version: 0.5.0 + Generated: 2023-06-01 19:00:52 UTC | Package | Version | Author(s) | License | | ---- | ---- | ---- | ---- |