commit 08d50f8dccd40eb74ef645c34a8c64c06e1363e3 Author: javalsai Date: Sun May 31 00:11:05 2026 +0200 initial version - preludes - slice::{RotateInsertExt, SplitOn}, cstr::{UnixCStrAs, CStrSplit}, str::UnixStrAs - exhaustive testing and checking - documentation for everything diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..d68f980 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,2 @@ +[codespell] +skip = ./target diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..cfae7e2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "proseWrap": "always", + "printWidth": 80 +} diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6dd3d8b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "boilerext" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..156ca6a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "boilerext" +description = "Rlib with useful boilerplate extension traits for QoL" +version = "0.1.0" +edition = "2024" +license = "MIT" +repository = "https://git.javalsai.tuxcord.net/rust/boilerext" +keywords = ["ext", "trait", "extension-trait"] +categories = ["rust-patterns"] + +[features] +default = [] + +[lints.clippy] +cargo = { level = "warn", priority = -1 } +correctness = { level = "deny", priority = -1 } +nursery = { level = "deny", priority = -1 } +option_if_let_else = { level = "allow" } + +pedantic = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +style = { level = "deny", priority = -1 } +unwrap_used = "deny" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b504113 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2026 javalsai + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..156d88e --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# boilerext + +Rlib with useful boilerplate extension traits for QoL. + +Aims to extend methods that have negligible performance cost, which only +reasonable barrier is their lenthyness or unintuitiveness. + +Uses unsafe rust where its safetyness is immediately obvious to squeeze +performance without fear of breaking safety contracts if the abstracted method +wasn't there. + +# Docs + +Documentation is not hosted publicly for now, clone and run `cargo doc` to +generate it. + +# Development + +This project uses cargo test/miri test/doc/fmt/check/clippy, codespell, prettier +and shellcheck to check and help in development. + +You can check for everything (as long as you have the tools) with +`scripts/check.sh`. For additional stuff pass `extra` as the first argument to +said script. + +# License + +This code is licensed under the MIT license, a copy can be found at the root of +this project. + +The philosophy of the project is just to provide small extension traits with +accessibility ease, so the MIT license was chosen to avoid legal friction. diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..a5dc5a8 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +doc-valid-idents = ["QoL", ".."] diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100755 index 0000000..1c01309 --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "$0")/.." + +ANSIRED="\033[1;31m" +ANSIRESET="\033[0m" +errprint() { + printf "%b%s%b\n" \ + "$ANSIRED" "$*" "$ANSIRESET" >&2 +} + +[ -f "Cargo.toml" ] || { + errprint "No Cargo.toml present" + exit 1 +} + +( + set -x + + cargo check -q + cargo clippy -q + cargo test -q + cargo doc -q + cargo fmt --check + codespell + prettier --check . + git ls-files -z "*.sh" | xargs -0 shellcheck +) + +if [ "${1:-}" == "extra" ]; then ( + set -x + + cargo +nightly miri test -q +) fi diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..be4fe1b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,205 @@ +#![doc = include_str!("../README.md")] + +pub mod prelude { + //! Prelude with extended method names that should never collide. + //! + //! Trait identifiers are not exported, only the implementation methods are. + //! + //! ``` + //! # use std::ffi::CStr; + //! use boilerext::prelude::*; + //! CStr::split_ch; + //! ``` + //! + //! ```compile_fail + //! use boilerext::prelude::*; + //! CStrSplit; + //! ``` + + pub use crate::{cstr::CStrSplit as _, slice::RotateInsertExt as _, slice::SplitOn as _}; +} + +pub mod unix_prelude { + //! Analogous to [`crate::prelude`] but only for traits exclusive to unix-based systems. + + pub use crate::{cstr::UnixCStrAs as _, str::UnixStrAs as _}; +} + +pub mod slice { + use std::borrow::Borrow; + + /// Rotates the slice like `rotate_right` while inserting a given value and returning the + /// popped-off elements. + pub trait RotateInsertExt { + /// Checked version of [`rotate_insert`](Self::rotate_insert) + /// + /// ``` + /// # use boilerext::slice::RotateInsertExt as _; + /// let array: &mut [u8] = &mut []; + /// assert_eq!(None, array[..].checked_rotate_insert([0])); + /// ``` + fn checked_rotate_insert(&mut self, insert: [T; N]) -> Option<[T; N]>; + + /// # Panics + /// + /// If the slice is smaller than `N` + /// + /// # Examples + /// + /// ``` + /// # use boilerext::slice::RotateInsertExt as _; + /// let array: &mut [u8] = &mut [0, 1, 2, 3, 4]; + /// // [ 2.. ]; + /// + /// // [5, 6] => [2, 3, 4] + /// // [5, 6, 2] => [3, 4] + /// let taken_out = array[2..].rotate_insert([5, 6]); + /// assert_eq!(array, &[0, 1, 5, 6, 2]); + /// assert_eq!(taken_out, [3, 4]); + /// ``` + /// + /// ```should_panic + /// # use boilerext::slice::RotateInsertExt as _; + /// let array: &mut [u8] = &mut []; + /// let taken_out = array[..].rotate_insert([0]); + /// ``` + fn rotate_insert(&mut self, insert: [T; N]) -> [T; N] { + self.checked_rotate_insert(insert) + .expect("slice should be bigger than the rotation") + } + } + + impl RotateInsertExt for [T] { + fn checked_rotate_insert(&mut self, mut insert: [T; N]) -> Option<[T; N]> { + if self.len() < N { + return None; + } + + self.rotate_right(N); + // swaps `insert` with the rotated part of `self` such that `insert` also becomes the + // return slice + for idx in 0..N { + std::mem::swap(&mut self[idx], &mut insert[idx]); + } + + Some(insert) + } + } + + /// Intended to resemble the currently nightly feature `slice_split_once` + /// ([#112811](https://github.com/rust-lang/rust/issues/112811)) but for a value instead of an + /// equality predicate. + pub trait SplitOn { + /// # Examples + /// + /// ``` + /// # use boilerext::slice::SplitOn as _; + /// let (l, r) = b"abcde".split_on(b'c').expect("to contain 'c'"); + /// assert_eq!(l, b"ab"); + /// assert_eq!(r, b"de"); + /// + /// let None = b"abcde".split_on(b'0') else { panic!("to not contain '0'"); }; + /// ``` + fn split_on(&self, value: impl Borrow) -> Option<(&[T], &[T])>; + + /// # Examples + /// + /// ``` + /// # use boilerext::slice::SplitOn as _; + /// let mut slice: [u8; _] = *b"abcde"; + /// let (l, r): (&mut _, &mut _) = slice.split_on_mut(b'c').expect("to contain 'c'"); + /// assert_eq!(l, b"ab"); + /// assert_eq!(r, b"de"); + /// + /// let None = slice.split_on_mut(b'0') else { panic!("to not contain '0'"); }; + /// ``` + fn split_on_mut(&mut self, value: impl Borrow) -> Option<(&mut [T], &mut [T])>; + } + + impl SplitOn for [T] { + fn split_on(&self, value: impl Borrow) -> Option<(&[T], &[T])> { + let (idx, _) = self + .iter() + .enumerate() + .find(|(_, b)| *b == value.borrow())?; + // SAFETY: idx is IN-OF-BOUNDS index of the slice + unsafe { Some((self.get_unchecked(0..idx), self.get_unchecked((idx + 1)..))) } + } + + fn split_on_mut(&mut self, value: impl Borrow) -> Option<(&mut [T], &mut [T])> { + let (idx, _) = self + .iter() + .enumerate() + .find(|(_, b)| *b == value.borrow())?; + // SAFETY: idx is IN-OF-BOUNDS index of the slice + let (a, b) = unsafe { self.split_at_mut_unchecked(idx) }; + Some((a, &mut b[1..])) // b includes index + } + } +} + +pub mod cstr { + use std::{ + ffi::{CStr, OsStr}, + num::NonZeroU8, + }; + + use crate::slice::SplitOn; + + /// Implements transparent `as_*` methods for unix systems. + pub trait UnixCStrAs { + fn as_os_str(&self) -> &OsStr; + } + + impl UnixCStrAs for CStr { + fn as_os_str(&self) -> &OsStr { + use std::os::unix::ffi::OsStrExt as _; + OsStr::from_bytes(self.to_bytes()) + } + } + + /// Implements split methods for [`CStr`]ings + pub trait CStrSplit { + /// # Examples + /// + /// ``` + /// # use std::num::NonZeroU8; + /// # use boilerext::cstr::CStrSplit; + /// let string = c"abcde"; + /// + /// let (l, r) = string.split_ch(NonZeroU8::new(b'c').unwrap()).expect("to contain 'c'"); + /// assert_eq!(l, b"ab"); + /// assert_eq!(r, c"de"); + /// ``` + fn split_ch(&self, ch: NonZeroU8) -> Option<(&[u8], &CStr)>; + } + + impl CStrSplit for CStr { + fn split_ch(&self, ch: NonZeroU8) -> Option<(&[u8], &CStr)> { + let self_slice = self.to_bytes_with_nul(); + + let (l, r) = self_slice.split_on(ch.get())?; + // SAFETY: split originated in a slice with a final NULL byte. + // Hence, second split, occurring before NULL byte because `ch` can't be 0, will include + // the final byte. + debug_assert!(r.ends_with(&[0])); + Some((l, unsafe { Self::from_bytes_with_nul_unchecked(r) })) + } + } +} + +pub mod str { + use std::ffi::OsStr; + + /// Implements transparent `as_*` methods for unix systems. + pub trait UnixStrAs { + fn as_os_str(&self) -> &OsStr; + } + + impl UnixStrAs for str { + fn as_os_str(&self) -> &OsStr { + use std::os::unix::ffi::OsStrExt as _; + OsStr::from_bytes(self.as_bytes()) + } + } +}