Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ jobs:
- name: Build
run: cargo build --no-default-features --workspace --target thumbv6m-none-eabi
shell: bash

build-no-std-with-alloc:
name: Build no_std with `alloc` feature enabled
runs-on: ubuntu-latest
steps:
- name: Checkout Sources
uses: actions/checkout@v6

- name: Install Rust Toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: thumbv6m-none-eabi

- name: Build
run: cargo build --no-default-features --features alloc --workspace --target thumbv6m-none-eabi
shell: bash

build-no-std-with-serde:
name: Build no_std with `serde` feature enabled
Expand Down
4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ all-features = true
[features]
default = ["std"]
std = ["unsigned-varint/std", "alloc"]
alloc = ["no_std_io2/alloc"]
alloc = []
arb = ["dep:quickcheck", "dep:rand", "dep:arbitrary"]
scale-codec = ["dep:parity-scale-codec"]
serde-codec = ["serde"] # Deprecated, don't use.
Expand All @@ -42,8 +42,6 @@ serde = { version = "1.0.116", optional = true, default-features = false }
unsigned-varint = { version = "0.8.0", default-features = false }
arbitrary = { version = "1.1.0", optional = true }

no_std_io2 = { workspace = true }

[dev-dependencies]
hex = "0.4.2"
serde_json = "1.0.58"
Expand Down
3 changes: 1 addition & 2 deletions codetable/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ rust-version = "1.81"

[features]
default = ["std"]
std = ["blake2b_simd?/std", "blake2s_simd?/std", "blake3?/std", "digest/alloc", "sha1?/alloc", "sha2?/alloc", "sha3?/alloc", "ripemd?/alloc", "multihash-derive/std", "no_std_io2/std"]
std = ["blake2b_simd?/std", "blake2s_simd?/std", "blake3?/std", "digest/alloc", "sha1?/alloc", "sha2?/alloc", "sha3?/alloc", "ripemd?/alloc", "multihash-derive/std"]
arb = ["dep:arbitrary", "std"]
sha1 = ["dep:sha1"]
sha2 = ["dep:sha2"]
Expand All @@ -32,7 +32,6 @@ sha3 = { version = "0.11", default-features = false, optional = true }
strobe-rs = { version = "0.13", default-features = false, optional = true }
ripemd = { version = "0.2", default-features = false, optional = true }
multihash-derive = { version = "0.9.2", path = "../derive", default-features = false }
no_std_io2 = { workspace = true }
digest = { version = "0.11", default-features = false }
serde = { version = "1.0.158", features = ["derive"], default-features = false, optional = true }
arbitrary = { version = "1.3.2", optional = true, features = ["derive"] }
Expand Down
14 changes: 8 additions & 6 deletions codetable/src/hasher_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
))]
macro_rules! derive_write {
($name:ident) => {
impl<const S: usize> no_std_io2::io::Write for $name<S> {
fn write(&mut self, buf: &[u8]) -> no_std_io2::io::Result<usize> {
#[cfg(feature = "std")]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make sense to just add something like this at the top:

#[cfg(feature = "std")]
use std::io;

#[cfg(not(feature = "std"))]
use crate::no_std_io as io;

Copy link
Copy Markdown
Contributor Author

@hanabi1224 hanabi1224 Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.
(Did not implement the custom Write trait to not allow downstream crates to depend on it. But that might not be an issue.)
(Top-level use may not work when the macro is used in another file, using the full trait name instead)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not implement the custom Write trait to not allow downstream crates to depend on it.

That's actually a good point. My thought was "just keep the current behaviour" without questioning it. I don't have much experience with no-std environments. I guess you would probably implement your own Write-like trait that fits your purpose. You wouldn't want to take a random, stripped down implementation from multihash.

@hanabi1224 it sounds like you've more experience with no-std. So sorry for the back and forth, but I think it makes sense to implement Write only for std. If if it turns out to be a problem for downstream users, they'll tell us.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vmx reverted.

impl<const S: usize> std::io::Write for $name<S> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
use multihash_derive::Hasher as _;

self.update(buf);
Ok(buf.len())
}

fn flush(&mut self) -> no_std_io2::io::Result<()> {
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
Expand Down Expand Up @@ -194,15 +195,16 @@ macro_rules! derive_rustcrypto_hasher {
}
}

impl no_std_io2::io::Write for $name {
fn write(&mut self, buf: &[u8]) -> no_std_io2::io::Result<usize> {
#[cfg(feature = "std")]
impl std::io::Write for $name {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
use multihash_derive::Hasher as _;

self.update(buf);
Ok(buf.len())
}

fn flush(&mut self) -> no_std_io2::io::Result<()> {
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#[cfg(not(feature = "std"))]
use no_std_io2::io;
use crate::no_std_io as io;
#[cfg(feature = "std")]
use std::io;

Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ extern crate alloc;
mod arb;
mod error;
mod multihash;
#[cfg(not(feature = "std"))]
pub mod no_std_io; // Make it public for downstream crates(e.g. `cid`).
#[cfg(feature = "serde")]
mod serde;

Expand Down
2 changes: 1 addition & 1 deletion src/multihash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use unsigned_varint::encode as varint_encode;
use std::io;

#[cfg(not(feature = "std"))]
use no_std_io2::io;
use crate::no_std_io as io;

/// A Multihash instance that only supports the basic functionality and no hashing.
///
Expand Down
68 changes: 68 additions & 0 deletions src/no_std_io/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use core::{fmt, result};

pub(super) type Result<T> = result::Result<T, Error>;

/// A minimal backfill of the [`std::io::Error`] for `no_std` environments.
#[derive(Debug)]
pub struct Error {
kind: ErrorKind,
error: &'static str,
}

impl core::error::Error for Error {}

/// A list specifying general categories of I/O error.
///
/// This list is intended to grow over time and it is not recommended to
/// exhaustively match against it.
///
/// It is used with the [`io::Error`] type.
///
/// [`io::Error`]: Error
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub(super) enum ErrorKind {
/// An error returned when an operation could not be completed because a
/// call to [`write`] returned [`Ok(0)`].
///
/// This typically means that an operation could only succeed if it wrote a
/// particular number of bytes but only a smaller number of bytes could be
/// written.
///
/// [`write`]: crate::io::Write::write
/// [`Ok(0)`]: Ok
WriteZero,
/// This operation was interrupted.
///
/// Interrupted operations can typically be retried.
Interrupted,
/// An error returned when an operation could not be completed because an
/// "end of file" was reached prematurely.
///
/// This typically means that an operation could only succeed if it read a
/// particular number of bytes but only a smaller number of bytes could be
/// read.
UnexpectedEof,
}

impl Error {
/// Creates a new I/O error from a known kind of error as well as an
/// arbitrary error payload.
///
/// This function is used to generically create I/O errors which do not
/// originate from the OS itself. The `error` argument is an arbitrary
/// payload which will be contained in this [`Error`].
pub(super) fn new(kind: ErrorKind, error: &'static str) -> Error {
Error { kind, error }
}

/// Returns the corresponding [`ErrorKind`] for this error.
pub(super) fn kind(&self) -> ErrorKind {
self.kind
}
}

impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
self.error.fmt(fmt)
}
}
113 changes: 113 additions & 0 deletions src/no_std_io/impls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use super::error::{Error, ErrorKind, Result};
use super::traits::{Read, Write};

impl<R: Read + ?Sized> Read for &mut R {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
(**self).read(buf)
}

#[inline]
fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> {
(**self).read_exact(buf)
}
}

impl<W: Write + ?Sized> Write for &mut W {
#[inline]
fn write(&mut self, buf: &[u8]) -> Result<usize> {
(**self).write(buf)
}
}

/// Read is implemented for `&[u8]` by copying from the slice.
///
/// Note that reading updates the slice to point to the yet unread part.
/// The slice will be empty when EOF is reached.
impl Read for &[u8] {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
let amt = core::cmp::min(buf.len(), self.len());
let (a, b) = self.split_at(amt);

// First check if the amount of bytes we want to read is small:
// `copy_from_slice` will generally expand to a call to `memcpy`, and
// for a single byte the overhead is significant.
if amt == 1 {
buf[0] = a[0];
} else {
buf[..amt].copy_from_slice(a);
}

*self = b;
Ok(amt)
}

#[inline]
fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> {
if buf.len() > self.len() {
return Err(Error::new(
ErrorKind::UnexpectedEof,
"failed to fill whole buffer",
));
}
let (a, b) = self.split_at(buf.len());

// First check if the amount of bytes we want to read is small:
// `copy_from_slice` will generally expand to a call to `memcpy`, and
// for a single byte the overhead is significant.
if buf.len() == 1 {
buf[0] = a[0];
} else {
buf.copy_from_slice(a);
}

*self = b;
Ok(())
}
}

/// Write is implemented for `&mut [u8]` by copying into the slice, overwriting
/// its data.
///
/// Note that writing updates the slice to point to the yet unwritten part.
/// The slice will be empty when it has been completely overwritten.
impl Write for &mut [u8] {
#[inline]
fn write(&mut self, data: &[u8]) -> Result<usize> {
let amt = core::cmp::min(data.len(), self.len());
let (a, b) = core::mem::take(self).split_at_mut(amt);
a.copy_from_slice(&data[..amt]);
*self = b;
Ok(amt)
}

#[inline]
fn write_all(&mut self, data: &[u8]) -> Result<()> {
if self.write(data)? == data.len() {
Ok(())
} else {
Err(Error::new(
ErrorKind::WriteZero,
"failed to write whole buffer",
))
}
}
}

/// Write is implemented for `Vec<u8>` by appending to the vector.
/// The vector will grow as needed.
#[cfg(feature = "alloc")]
impl Write for alloc::vec::Vec<u8> {
#[inline]
fn write(&mut self, buf: &[u8]) -> Result<usize> {
self.extend_from_slice(buf);
Ok(buf.len())
}

#[inline]
fn write_all(&mut self, buf: &[u8]) -> Result<()> {
self.extend_from_slice(buf);
Ok(())
}
}
11 changes: 11 additions & 0 deletions src/no_std_io/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! No-std I/O utilities for the multihash crate.
//!
//! This module provides a minimal compatibility layer for I/O operations in `no_std` environments.
//! Source code is ported and adapted from the [`core2`](https://docs.rs/crate/core2/0.4.0/source/) crate.

mod error;
mod impls;
mod traits;

pub use error::Error;
pub use traits::{Read, Write};
54 changes: 54 additions & 0 deletions src/no_std_io/traits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use super::error::{Error, ErrorKind, Result};

/// A minimal backfill of the [`std::io::Read`] for `no_std` environments.
pub trait Read {
/// Backfill of the [`std::io::Read::read`].
fn read(&mut self, buf: &mut [u8]) -> Result<usize>;

/// Backfill of the [`std::io::Read::read_exact`].
fn read_exact(&mut self, mut buf: &mut [u8]) -> Result<()> {
while !buf.is_empty() {
match self.read(buf) {
Ok(0) => break,
Ok(n) => {
let tmp = buf;
buf = &mut tmp[n..];
}
Err(ref e) if e.kind() == ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
if !buf.is_empty() {
Err(Error::new(
ErrorKind::UnexpectedEof,
"failed to fill whole buffer",
))
} else {
Ok(())
}
}
}

/// A minimal backfill of the [`std::io::Write`] for `no_std` environments.
pub trait Write {
/// Backfill of the [`std::io::Write::write`].
fn write(&mut self, buf: &[u8]) -> Result<usize>;

/// Backfill of the [`std::io::Write::write_all`].
fn write_all(&mut self, mut buf: &[u8]) -> Result<()> {
while !buf.is_empty() {
match self.write(buf) {
Ok(0) => {
return Err(Error::new(
ErrorKind::WriteZero,
"failed to write whole buffer",
));
}
Ok(n) => buf = &buf[n..],
Err(ref e) if e.kind() == ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(())
}
}