From 08d50f8dccd40eb74ef645c34a8c64c06e1363e3 Mon Sep 17 00:00:00 2001 From: javalsai Date: Sun, 31 May 2026 00:11:05 +0200 Subject: [PATCH] initial version - preludes - slice::{RotateInsertExt, SplitOn}, cstr::{UnixCStrAs, CStrSplit}, str::UnixStrAs - exhaustive testing and checking - documentation for everything --- .codespellrc | 2 + .gitignore | 1 + .prettierrc | 4 + Cargo.lock | 7 ++ Cargo.toml | 23 ++++++ LICENSE | 19 +++++ README.md | 32 ++++++++ clippy.toml | 1 + scripts/check.sh | 36 +++++++++ src/lib.rs | 205 +++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 330 insertions(+) create mode 100644 .codespellrc create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 clippy.toml create mode 100755 scripts/check.sh create mode 100644 src/lib.rs 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()) + } + } +}