From 40e09a16b3b38d5aaea1fbd30ce67d2a7455299b Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 19 Apr 2026 18:32:23 -0500 Subject: [PATCH 1/5] make crate no_std compatible Previously, the crate unconditionally depended on `std`. With this commit, the crate is marked `#![no_std]` and uses `alloc` instead of `std` for its core functionality. An opt-in `std` feature gates the `io::Write`-based APIs (`Patch::to_bytes`, `PatchFormatter::write_*_into`) and the `std::error::Error` impls on error types. `HashMap` usage is now sourced from `hashbrown` rather than `std::collections` so line classification works without `std`. Tests now require `--all-features` to run since they exercise the `std`-gated APIs and rely on `alloc` prelude imports. --- .github/workflows/ci.yml | 3 +-- Cargo.lock | 16 ++++++++++++++++ Cargo.toml | 7 +++++-- src/apply.rs | 9 +++++++-- src/binary/base85.rs | 5 ++++- src/binary/delta.rs | 5 ++++- src/binary/mod.rs | 11 +++++++++-- src/diff/cleanup.rs | 1 + src/diff/mod.rs | 6 +++++- src/diff/myers.rs | 9 ++++++--- src/diff/tests.rs | 11 +++++++++-- src/lib.rs | 8 ++++++++ src/merge/mod.rs | 5 ++++- src/patch/error.rs | 5 +++-- src/patch/format.rs | 15 +++++++++++---- src/patch/mod.rs | 19 +++++++++++++------ src/patch/parse.rs | 7 ++++--- src/patch/tests.rs | 4 ++++ src/patch_set/error.rs | 6 ++++-- src/patch_set/mod.rs | 24 ++++++++++++------------ src/patch_set/parse.rs | 3 ++- src/patch_set/tests.rs | 4 ++++ src/range.rs | 2 +- src/utils.rs | 18 ++++++++++-------- 24 files changed, 147 insertions(+), 56 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 481f8788..455ca13c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,8 +29,7 @@ jobs: - uses: actions/checkout@v6 - run: rustup toolchain install ${{ matrix.rust }} --profile minimal - run: cargo +${{ matrix.rust }} check --all-targets --all-features - - run: cargo +${{ matrix.rust }} test - - run: cargo +${{ matrix.rust }} test -F binary + - run: cargo +${{ matrix.rust }} test --all-features lint: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index c726b1cd..3c5f411e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,7 @@ version = "0.4.2" dependencies = [ "anstyle", "flate2", + "hashbrown", "rayon", "snapbox", ] @@ -169,6 +170,12 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "getrandom" version = "0.3.4" @@ -181,6 +188,15 @@ dependencies = [ "wasip2", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" diff --git a/Cargo.toml b/Cargo.toml index 125a3c72..6e2c1703 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,14 @@ rust-version = "1.75.0" edition = "2021" [features] -binary = ["dep:flate2"] +default = [] +std = [] +binary = ["dep:flate2", "std"] color = ["dep:anstyle"] [dependencies] -anstyle = { version = "1.0.13", optional = true } +hashbrown = { version = "0.16.1", default-features = false, features = ["default-hasher"] } +anstyle = { version = "1.0.13", default-features = false, optional = true } flate2 = { version = "1.1.9", optional = true, default-features = false, features = ["zlib-rs"] } [dev-dependencies] diff --git a/src/apply.rs b/src/apply.rs index 9b971cf2..cf1036ac 100644 --- a/src/apply.rs +++ b/src/apply.rs @@ -2,7 +2,11 @@ use crate::{ patch::{Hunk, Line, Patch}, utils::LineIter, }; -use std::{fmt, iter}; +use alloc::string::String; +use alloc::vec::Vec; +use core::cmp; +use core::fmt; +use core::iter; /// An error returned when [`apply`]ing a `Patch` fails /// @@ -16,6 +20,7 @@ impl fmt::Display for ApplyError { } } +#[cfg(feature = "std")] impl std::error::Error for ApplyError {} #[derive(Debug)] @@ -145,7 +150,7 @@ fn find_position( ) -> Option { // In order to avoid searching through positions which are out of bounds of the image, // clamp the starting position based on the length of the image - let pos = std::cmp::min(hunk.new_range().start().saturating_sub(1), image.len()); + let pos = cmp::min(hunk.new_range().start().saturating_sub(1), image.len()); // Create an iterator that starts with 'pos' and then interleaves // moving pos backward/foward by one. diff --git a/src/binary/base85.rs b/src/binary/base85.rs index 62cb832b..dbc6440b 100644 --- a/src/binary/base85.rs +++ b/src/binary/base85.rs @@ -7,7 +7,7 @@ //! //! [RFC 1924]: https://datatracker.ietf.org/doc/html/rfc1924 -use std::fmt; +use core::fmt; /// Base85 character set (RFC 1924). const ALPHABET: &[u8; 85] = b"0123456789\ @@ -46,6 +46,7 @@ impl fmt::Display for Base85Error { } } +#[cfg(feature = "std")] impl std::error::Error for Base85Error {} /// Decodes a Base85 string to the provided output. @@ -115,6 +116,8 @@ pub fn encode_into(input: &[u8], output: &mut impl Extend) -> Result<(), B #[cfg(test)] mod tests { use super::*; + use alloc::string::String; + use alloc::vec::Vec; fn decode(input: &str) -> Result, Base85Error> { let mut result = Vec::with_capacity((input.len() / 5) * 4); diff --git a/src/binary/delta.rs b/src/binary/delta.rs index 7ab42bf6..2f0db245 100644 --- a/src/binary/delta.rs +++ b/src/binary/delta.rs @@ -7,7 +7,8 @@ //! //! Based on Diffx's [Git Delta Binary Diffs](https://diffx.org/spec/binary-diffs.html#git-delta-binary-diffs) -use std::fmt; +use alloc::vec::Vec; +use core::fmt; /// Applies delta instructions to an original file, producing the modified file. pub fn apply(original: &[u8], delta: &[u8]) -> Result, DeltaError> { @@ -220,11 +221,13 @@ impl fmt::Display for DeltaError { } } +#[cfg(feature = "std")] impl std::error::Error for DeltaError {} #[cfg(test)] mod tests { use super::*; + use alloc::vec; #[test] fn read_size_single_byte() { diff --git a/src/binary/mod.rs b/src/binary/mod.rs index e41226d8..77794a2f 100644 --- a/src/binary/mod.rs +++ b/src/binary/mod.rs @@ -10,7 +10,11 @@ mod base85; #[cfg(feature = "binary")] mod delta; -use std::{fmt, ops::Range}; +#[cfg(feature = "binary")] +use alloc::string::String; +#[cfg(feature = "binary")] +use alloc::vec::Vec; +use core::{fmt, ops::Range}; /// Cap preallocation when the size comes from untrusted input. /// @@ -115,6 +119,7 @@ impl<'a> BinaryPatch<'a> { /// See [Decoding Logic](https://diffx.org/spec/binary-diffs.html#decoding-logic) #[cfg(feature = "binary")] fn decode_data(binary_data: &BinaryData<'_>) -> Result, BinaryPatchParseError> { + use alloc::string::ToString; use std::io::Read; let compressed = decode_base85_lines(binary_data.data)?; @@ -193,6 +198,7 @@ impl fmt::Display for BinaryPatchParseError { } } +#[cfg(feature = "std")] impl std::error::Error for BinaryPatchParseError {} #[cfg(feature = "binary")] @@ -358,7 +364,7 @@ fn parse_binary_block<'a>(parser: &mut BinaryParser<'a>) -> Option(solution: &[DiffRange<[T]>]) -> Vec { #[cfg(test)] mod test { use super::DiffOptions; + use alloc::string::ToString; #[test] fn set_original_and_modified_filenames() { diff --git a/src/diff/myers.rs b/src/diff/myers.rs index 502d0772..cc4cd4ec 100644 --- a/src/diff/myers.rs +++ b/src/diff/myers.rs @@ -1,5 +1,8 @@ use crate::range::{DiffRange, Range}; -use std::ops::{Index, IndexMut}; +use alloc::vec; +use alloc::vec::Vec; +use core::fmt; +use core::ops::{Index, IndexMut}; // A D-path is a path which starts at (0,0) that has exactly D non-diagonal edges. All D-paths // consist of a (D - 1)-path followed by a non-diagonal edge and then a possibly empty sequence of @@ -57,8 +60,8 @@ struct Snake { y_end: usize, } -impl ::std::fmt::Display for Snake { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { +impl fmt::Display for Snake { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "({}, {}) -> ({}, {})", diff --git a/src/diff/tests.rs b/src/diff/tests.rs index 050c7510..17f77426 100644 --- a/src/diff/tests.rs +++ b/src/diff/tests.rs @@ -6,6 +6,10 @@ use crate::{ range::Range, PatchFormatter, }; +use alloc::format; +use alloc::string::ToString; +use alloc::vec; +use alloc::vec::Vec; // Helper macros are based off of the ones used in [dissimilar](https://docs.rs/dissimilar) macro_rules! diff_range_list { @@ -764,8 +768,11 @@ Second: let elapsed = now.elapsed(); - println!("{:?}", elapsed); - assert!(elapsed < std::time::Duration::from_micros(200)); + assert!( + elapsed < std::time::Duration::from_micros(200), + "{:?}", + elapsed + ); assert_eq!(result, expected); } diff --git a/src/lib.rs b/src/lib.rs index 5e11a40d..193c3565 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -222,6 +222,14 @@ //! [`create_patch`]: fn.create_patch.html //! [`create_patch_bytes`]: fn.create_patch_bytes.html +// unconditionally define as no_std to have consistency on the prelude that is auto imported. +#![no_std] + +extern crate alloc; + +#[cfg(feature = "std")] +extern crate std; + mod apply; pub mod binary; mod diff; diff --git a/src/merge/mod.rs b/src/merge/mod.rs index 83b99fbc..6e3c7341 100644 --- a/src/merge/mod.rs +++ b/src/merge/mod.rs @@ -3,7 +3,10 @@ use crate::{ range::{DiffRange, Range, SliceLike}, utils::Classifier, }; -use std::{cmp, fmt}; +use alloc::fmt; +use alloc::string::String; +use alloc::vec::Vec; +use core::cmp; #[cfg(test)] mod tests; diff --git a/src/patch/error.rs b/src/patch/error.rs index eecf8767..d63560d8 100644 --- a/src/patch/error.rs +++ b/src/patch/error.rs @@ -1,7 +1,7 @@ //! Error types for patch parsing. -use std::fmt; -use std::ops::Range; +use core::fmt; +use core::ops::Range; /// An error returned when parsing a `Patch` using [`Patch::from_str`] fails. /// @@ -36,6 +36,7 @@ impl fmt::Display for ParsePatchError { } } +#[cfg(feature = "std")] impl std::error::Error for ParsePatchError {} impl From for ParsePatchError { diff --git a/src/patch/format.rs b/src/patch/format.rs index 1c8e38fd..eaeddb5a 100644 --- a/src/patch/format.rs +++ b/src/patch/format.rs @@ -1,7 +1,8 @@ -use std::{ - fmt::{Display, Formatter, Result}, - io, -}; +use alloc::borrow::ToOwned; +use core::fmt::{Display, Formatter, Result}; + +#[cfg(feature = "std")] +use std::io; #[cfg(feature = "color")] use super::style; @@ -69,6 +70,7 @@ impl PatchFormatter { PatchDisplay { f: self, patch } } + #[cfg(feature = "std")] pub fn write_patch_into + ?Sized, W: io::Write>( &self, patch: &Patch<'_, T>, @@ -81,6 +83,7 @@ impl PatchFormatter { HunkDisplay { f: self, hunk } } + #[cfg(feature = "std")] fn write_hunk_into + ?Sized, W: io::Write>( &self, hunk: &Hunk<'_, T>, @@ -93,6 +96,7 @@ impl PatchFormatter { LineDisplay { f: self, line } } + #[cfg(feature = "std")] fn write_line_into + ?Sized, W: io::Write>( &self, line: &Line<'_, T>, @@ -113,6 +117,7 @@ struct PatchDisplay<'a, T: ToOwned + ?Sized> { patch: &'a Patch<'a, T>, } +#[cfg(feature = "std")] impl + ?Sized> PatchDisplay<'_, T> { fn write_into(&self, mut w: W) -> io::Result<()> { if self.patch.original.is_some() || self.patch.modified.is_some() { @@ -176,6 +181,7 @@ struct HunkDisplay<'a, T: ?Sized> { hunk: &'a Hunk<'a, T>, } +#[cfg(feature = "std")] impl + ?Sized> HunkDisplay<'_, T> { fn write_into(&self, mut w: W) -> io::Result<()> { #[cfg(feature = "color")] @@ -250,6 +256,7 @@ struct LineDisplay<'a, T: ?Sized> { line: &'a Line<'a, T>, } +#[cfg(feature = "std")] impl + ?Sized> LineDisplay<'_, T> { fn write_into(&self, mut w: W) -> io::Result<()> { #[cfg(feature = "color")] diff --git a/src/patch/mod.rs b/src/patch/mod.rs index a26b0955..a195580e 100644 --- a/src/patch/mod.rs +++ b/src/patch/mod.rs @@ -9,9 +9,14 @@ mod tests; pub use error::ParsePatchError; pub use format::PatchFormatter; -use std::{borrow::Cow, fmt, ops}; +use alloc::borrow::{Cow, ToOwned}; +use alloc::fmt; +use alloc::vec::Vec; +use core::ops; -use crate::utils::{byte_needs_quoting, fmt_escaped_byte, write_escaped_byte}; +#[cfg(feature = "std")] +use crate::utils::write_escaped_byte; +use crate::utils::{byte_needs_quoting, fmt_escaped_byte}; const NO_NEWLINE_AT_EOF: &str = "\\ No newline at end of file"; @@ -102,6 +107,7 @@ impl<'a, T: ToOwned + ?Sized> Patch<'a, T> { } } +#[cfg(feature = "std")] impl + ToOwned + ?Sized> Patch<'_, T> { /// Convert a `Patch` into bytes /// @@ -186,7 +192,7 @@ impl fmt::Display for Patch<'_, str> { impl fmt::Debug for Patch<'_, T> where T: ToOwned + fmt::Debug, - O: std::borrow::Borrow + fmt::Debug, + O: core::borrow::Borrow + fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Patch") @@ -200,6 +206,7 @@ where #[derive(PartialEq, Eq)] struct Filename<'a, T: ToOwned + ?Sized>(Cow<'a, T>); +#[cfg(feature = "std")] impl + ?Sized> Filename<'_, T> { fn needs_to_be_escaped_bytes(&self) -> bool { self.0 @@ -245,10 +252,10 @@ impl Clone for Filename<'_, T> { } impl fmt::Display for Filename<'_, str> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let bytes = self.0.as_bytes(); if bytes.iter().any(|b| byte_needs_quoting(*b)) { - use std::fmt::Write; + use core::fmt::Write; f.write_char('\"')?; for &b in bytes { fmt_escaped_byte(f, b)?; @@ -265,7 +272,7 @@ impl fmt::Display for Filename<'_, str> { impl fmt::Debug for Filename<'_, T> where T: ToOwned + fmt::Debug, - O: std::borrow::Borrow + fmt::Debug, + O: core::borrow::Borrow + fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Filename").field(&self.0).finish() diff --git a/src/patch/parse.rs b/src/patch/parse.rs index 3d1fb26c..520e83db 100644 --- a/src/patch/parse.rs +++ b/src/patch/parse.rs @@ -8,9 +8,10 @@ use crate::{ patch::Patch, utils::{escaped_filename, LineIter, Text}, }; -use std::borrow::Cow; +use alloc::borrow::Cow; +use alloc::vec::Vec; -type Result = std::result::Result; +type Result = core::result::Result; /// Options that control parsing behavior. /// @@ -50,7 +51,7 @@ impl ParseOpts { } struct Parser<'a, T: Text + ?Sized> { - lines: std::iter::Peekable>, + lines: core::iter::Peekable>, offset: usize, } diff --git a/src/patch/tests.rs b/src/patch/tests.rs index 362dc373..e877c7c4 100644 --- a/src/patch/tests.rs +++ b/src/patch/tests.rs @@ -1,5 +1,7 @@ use super::error::ParsePatchErrorKind; use super::parse::{parse, parse_bytes, parse_bytes_strict, parse_strict}; +use alloc::format; +use alloc::string::ToString; #[test] fn trailing_garbage_after_complete_hunk() { @@ -637,6 +639,8 @@ fn non_utf8_escaped_filename_returns_error_on_str_parse() { } mod error_display { + use alloc::string::ToString; + use crate::patch::error::ParsePatchErrorKind; use crate::Patch; use snapbox::assert_data_eq; diff --git a/src/patch_set/error.rs b/src/patch_set/error.rs index 2c9116b6..1354edea 100644 --- a/src/patch_set/error.rs +++ b/src/patch_set/error.rs @@ -1,7 +1,8 @@ //! Error types for patches parsing. -use std::fmt; -use std::ops::Range; +use alloc::string::String; +use core::fmt; +use core::ops::Range; use crate::binary::BinaryPatchParseError; use crate::patch::ParsePatchError; @@ -42,6 +43,7 @@ impl fmt::Display for PatchSetParseError { } } +#[cfg(feature = "std")] impl std::error::Error for PatchSetParseError {} impl From for PatchSetParseError { diff --git a/src/patch_set/mod.rs b/src/patch_set/mod.rs index 16ec2808..8e921ea1 100644 --- a/src/patch_set/mod.rs +++ b/src/patch_set/mod.rs @@ -8,8 +8,8 @@ mod parse; #[cfg(test)] mod tests; -use std::borrow::Cow; -use std::fmt; +use alloc::borrow::{Cow, ToOwned}; +use core::fmt; use crate::binary::BinaryPatch; use crate::utils::Text; @@ -114,7 +114,7 @@ pub enum FileMode { Gitlink, } -impl std::str::FromStr for FileMode { +impl core::str::FromStr for FileMode { type Err = PatchSetParseError; fn from_str(mode: &str) -> Result { @@ -137,12 +137,12 @@ pub enum PatchKind<'a, T: ToOwned + ?Sized> { Binary(BinaryPatch<'a>), } -impl std::fmt::Debug for PatchKind<'_, T> +impl fmt::Debug for PatchKind<'_, T> where - T: ToOwned + std::fmt::Debug, - O: std::borrow::Borrow + std::fmt::Debug, + T: ToOwned + fmt::Debug, + O: core::borrow::Borrow + fmt::Debug, { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PatchKind::Text(patch) => f.debug_tuple("Text").field(patch).finish(), PatchKind::Binary(patch) => f.debug_tuple("Binary").field(patch).finish(), @@ -186,12 +186,12 @@ pub struct FilePatch<'a, T: ToOwned + ?Sized> { new_mode: Option, } -impl std::fmt::Debug for FilePatch<'_, T> +impl fmt::Debug for FilePatch<'_, T> where - T: ToOwned + std::fmt::Debug, - O: std::borrow::Borrow + std::fmt::Debug, + T: ToOwned + fmt::Debug, + O: core::borrow::Borrow + fmt::Debug, { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("FilePatch") .field("operation", &self.operation) .field("kind", &self.kind) @@ -320,7 +320,7 @@ impl Clone for FileOperation<'_, T> { impl fmt::Debug for FileOperation<'_, T> where T: ToOwned + fmt::Debug, - O: std::borrow::Borrow + fmt::Debug, + O: core::borrow::Borrow + fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { diff --git a/src/patch_set/parse.rs b/src/patch_set/parse.rs index a72b8234..86ccd812 100644 --- a/src/patch_set/parse.rs +++ b/src/patch_set/parse.rs @@ -9,7 +9,8 @@ use crate::patch::parse::parse_one; use crate::utils::{escaped_filename, Text}; use crate::Patch; -use std::borrow::Cow; +use alloc::borrow::Cow; +use alloc::string::String; /// Prefix for the original file path (e.g., `--- a/file.rs`). const ORIGINAL_PREFIX: &str = "--- "; diff --git a/src/patch_set/tests.rs b/src/patch_set/tests.rs index d1ff8804..81057ef1 100644 --- a/src/patch_set/tests.rs +++ b/src/patch_set/tests.rs @@ -1,5 +1,9 @@ //! Tests for patchset parsing. +use alloc::borrow::ToOwned; +use alloc::string::ToString; +use alloc::vec::Vec; + use super::{error::PatchSetParseErrorKind, FileOperation, ParseOptions, PatchSet}; mod file_operation { diff --git a/src/range.rs b/src/range.rs index eee04425..00b0b345 100644 --- a/src/range.rs +++ b/src/range.rs @@ -1,4 +1,4 @@ -use std::{cmp, fmt::Debug, ops}; +use core::{cmp, fmt::Debug, ops}; // Range type inspired by the Range type used in [dissimilar](https://docs.rs/dissimilar) #[derive(Debug)] diff --git a/src/utils.rs b/src/utils.rs index c4914566..d071a2c5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,10 +1,11 @@ //! Common utilities -use std::{ - borrow::Cow, - collections::{hash_map::Entry, HashMap}, - hash::Hash, -}; +use alloc::borrow::{Cow, ToOwned}; +use alloc::string::String; +use alloc::vec::Vec; +use core::hash::Hash; +use hashbrown::hash_map::Entry; +use hashbrown::HashMap; use crate::{patch::error::ParsePatchErrorKind, ParsePatchError}; @@ -22,6 +23,7 @@ pub(crate) fn byte_needs_quoting(b: u8) -> bool { /// `\t`, `\n`, `\v`, `\f`, `\r`, `\\`, `\"`). Other bytes that /// require quoting are emitted as 3-digit octal (`\NNN`). /// Non-special bytes are written through unchanged. +#[cfg(feature = "std")] pub(crate) fn write_escaped_byte(w: &mut W, b: u8) -> std::io::Result<()> { match b { b'\x07' => w.write_all(b"\\a"), @@ -49,7 +51,7 @@ pub(crate) fn write_escaped_byte(w: &mut W, b: u8) -> std::io /// Writes one byte in its escaped form to a [`fmt::Write`] sink. /// /// Same logic as [`write_escaped_byte`] but for [`fmt::Write`]. -pub(crate) fn fmt_escaped_byte(f: &mut impl std::fmt::Write, b: u8) -> std::fmt::Result { +pub(crate) fn fmt_escaped_byte(f: &mut impl core::fmt::Write, b: u8) -> core::fmt::Result { match b { b'\x07' => f.write_str("\\a"), b'\x08' => f.write_str("\\b"), @@ -154,7 +156,7 @@ pub trait Text: Eq + Hash + ToOwned { /// (e.g. non-UTF-8 bytes for `str`). fn owned_from_bytes(bytes: Vec) -> Option; - fn parse(&self) -> Option { + fn parse(&self) -> Option { self.as_str().and_then(|s| s.parse().ok()) } } @@ -256,7 +258,7 @@ impl Text for [u8] { } fn as_str(&self) -> Option<&str> { - std::str::from_utf8(self).ok() + core::str::from_utf8(self).ok() } fn as_bytes(&self) -> &[u8] { From 179e86544497ad9c60682572919bcb51317ca008 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 19 Apr 2026 18:44:58 -0500 Subject: [PATCH 2/5] docsrs: setup doc_cfg for feature gated items --- Cargo.toml | 15 ++++++++++++++- src/apply.rs | 1 + src/binary/base85.rs | 1 + src/binary/delta.rs | 1 + src/binary/mod.rs | 3 +++ src/lib.rs | 1 + src/patch/error.rs | 1 + src/patch/format.rs | 2 ++ src/patch/mod.rs | 1 + src/patch_set/error.rs | 1 + 10 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6e2c1703..291b78e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,19 @@ categories = ["text-processing"] rust-version = "1.75.0" edition = "2021" +[package.metadata.docs.rs] +# To build locally: +# RUSTDOCFLAGS="--cfg=docsrs -Zunstable-options --generate-link-to-definition" RUSTC_BOOTSTRAP=1 cargo doc --all-features --no-deps --open +all-features = true +rustdoc-args = [ + # Enable doc_cfg showing the required features. + "--cfg=docsrs", + + # Generate links to definition in rustdoc source code pages + # https://github.com/rust-lang/rust/pull/84176 + "-Zunstable-options", "--generate-link-to-definition" +] + [features] default = [] std = [] @@ -29,7 +42,7 @@ snapbox = { version = "0.6.24", features = ["dir"] } [[example]] name = "patch_formatter" -required-features = ["color"] +required-features = ["std", "color"] [[test]] name = "compat" diff --git a/src/apply.rs b/src/apply.rs index cf1036ac..a319f42a 100644 --- a/src/apply.rs +++ b/src/apply.rs @@ -21,6 +21,7 @@ impl fmt::Display for ApplyError { } #[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl std::error::Error for ApplyError {} #[derive(Debug)] diff --git a/src/binary/base85.rs b/src/binary/base85.rs index dbc6440b..600adf6b 100644 --- a/src/binary/base85.rs +++ b/src/binary/base85.rs @@ -47,6 +47,7 @@ impl fmt::Display for Base85Error { } #[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl std::error::Error for Base85Error {} /// Decodes a Base85 string to the provided output. diff --git a/src/binary/delta.rs b/src/binary/delta.rs index 2f0db245..fb6f2fee 100644 --- a/src/binary/delta.rs +++ b/src/binary/delta.rs @@ -222,6 +222,7 @@ impl fmt::Display for DeltaError { } #[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl std::error::Error for DeltaError {} #[cfg(test)] diff --git a/src/binary/mod.rs b/src/binary/mod.rs index 77794a2f..556c1d27 100644 --- a/src/binary/mod.rs +++ b/src/binary/mod.rs @@ -83,6 +83,7 @@ impl<'a> BinaryPatch<'a> { /// /// Unlike `git apply`, this doesn't validate the original content hash. #[cfg(feature = "binary")] + #[cfg_attr(docsrs, doc(cfg(feature = "binary")))] pub fn apply(&self, original: &[u8]) -> Result, BinaryPatchParseError> { match self { BinaryPatch::Full { forward, .. } => Self::apply_block(forward, original), @@ -97,6 +98,7 @@ impl<'a> BinaryPatch<'a> { /// /// Unlike `git apply`, this doesn't validate the modified content hash. #[cfg(feature = "binary")] + #[cfg_attr(docsrs, doc(cfg(feature = "binary")))] pub fn apply_reverse(&self, modified: &[u8]) -> Result, BinaryPatchParseError> { match self { BinaryPatch::Full { reverse, .. } => Self::apply_block(reverse, modified), @@ -199,6 +201,7 @@ impl fmt::Display for BinaryPatchParseError { } #[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl std::error::Error for BinaryPatchParseError {} #[cfg(feature = "binary")] diff --git a/src/lib.rs b/src/lib.rs index 193c3565..d14cb042 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -224,6 +224,7 @@ // unconditionally define as no_std to have consistency on the prelude that is auto imported. #![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] extern crate alloc; diff --git a/src/patch/error.rs b/src/patch/error.rs index d63560d8..be3ffa9e 100644 --- a/src/patch/error.rs +++ b/src/patch/error.rs @@ -37,6 +37,7 @@ impl fmt::Display for ParsePatchError { } #[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl std::error::Error for ParsePatchError {} impl From for ParsePatchError { diff --git a/src/patch/format.rs b/src/patch/format.rs index eaeddb5a..02e5652f 100644 --- a/src/patch/format.rs +++ b/src/patch/format.rs @@ -33,6 +33,7 @@ impl PatchFormatter { /// Enable formatting a patch with color #[cfg(feature = "color")] + #[cfg_attr(docsrs, doc(cfg(feature = "color")))] pub fn with_color(mut self) -> Self { self.with_color = true; self @@ -71,6 +72,7 @@ impl PatchFormatter { } #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn write_patch_into + ?Sized, W: io::Write>( &self, patch: &Patch<'_, T>, diff --git a/src/patch/mod.rs b/src/patch/mod.rs index a195580e..0665016c 100644 --- a/src/patch/mod.rs +++ b/src/patch/mod.rs @@ -108,6 +108,7 @@ impl<'a, T: ToOwned + ?Sized> Patch<'a, T> { } #[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl + ToOwned + ?Sized> Patch<'_, T> { /// Convert a `Patch` into bytes /// diff --git a/src/patch_set/error.rs b/src/patch_set/error.rs index 1354edea..6a169445 100644 --- a/src/patch_set/error.rs +++ b/src/patch_set/error.rs @@ -44,6 +44,7 @@ impl fmt::Display for PatchSetParseError { } #[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl std::error::Error for PatchSetParseError {} impl From for PatchSetParseError { From a8ae03347aa635830624b0e94c622425f5367ebf Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 19 Apr 2026 18:49:28 -0500 Subject: [PATCH 3/5] ci: check all feature combinations compile --- .github/workflows/ci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 455ca13c..1e046d76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,18 @@ jobs: - run: cargo clippy --all-targets --all-features - run: cargo doc --no-deps --all-features + check-features: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: rust version + run: | + rustc --version + cargo --version + - uses: taiki-e/install-action@cargo-hack + - run: cargo hack check --feature-powerset --no-dev-deps + semver: runs-on: ubuntu-latest steps: From 9fd107a77bd1104c2f979e58f5f9f941fa9a87ef Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 19 Apr 2026 18:53:40 -0500 Subject: [PATCH 4/5] chore(lints): warn on std/alloc/core confusion Enable the `std_instead_of_core`, `std_instead_of_alloc`, and `alloc_instead_of_core` clippy restriction lints so accidental reliance on `std::` in `no_std` code paths is caught at lint time. --- src/diff/tests.rs | 2 +- src/lib.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/diff/tests.rs b/src/diff/tests.rs index 17f77426..c4405742 100644 --- a/src/diff/tests.rs +++ b/src/diff/tests.rs @@ -769,7 +769,7 @@ Second: let elapsed = now.elapsed(); assert!( - elapsed < std::time::Duration::from_micros(200), + elapsed < core::time::Duration::from_micros(200), "{:?}", elapsed ); diff --git a/src/lib.rs b/src/lib.rs index d14cb042..e9b00d40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -224,6 +224,9 @@ // unconditionally define as no_std to have consistency on the prelude that is auto imported. #![no_std] +#![warn(clippy::std_instead_of_core)] +#![warn(clippy::std_instead_of_alloc)] +#![warn(clippy::alloc_instead_of_core)] #![cfg_attr(docsrs, feature(doc_cfg))] extern crate alloc; From d949662683035cac5d5bd04030df270e194bf281 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 19 Apr 2026 18:54:24 -0500 Subject: [PATCH 5/5] chore(ci): verify no_std build on aarch64-unknown-none MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, nothing in CI proved the crate actually works in `no_std` environments — a desktop `cargo build` with `std` omitted from features still resolves `std` through the target's sysroot. With this commit, add a dedicated job that builds against `aarch64-unknown-none`, a bare-metal target without any `std` crate, so any accidental `std` reliance fails to link. The job builds both the default configuration and `--features color` to cover the feature matrix that is expected to work without `std`. --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e046d76..ebc5b4ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,17 @@ jobs: - uses: taiki-e/install-action@cargo-hack - run: cargo hack check --feature-powerset --no-dev-deps + no-std: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - run: rustup toolchain install stable --profile minimal + - run: rustup target add aarch64-unknown-none + # Build against a bare-metal target that has no `std` crate, + # so any accidental reliance on `std` fails to link. + - run: cargo build --target aarch64-unknown-none --no-default-features + - run: cargo build --target aarch64-unknown-none --no-default-features --features color + semver: runs-on: ubuntu-latest steps: