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
17 changes: 15 additions & 2 deletions .github/workflows/nix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,14 @@ jobs:
use-flakehub: false

# Cross-compile to a bare-metal target that has no `std` at all, so a
# `no_std` regression cannot slip through.
- name: Build no_std (bare-metal target)
# `no_std` regression cannot slip through. The no-features build also has
# no allocator, so it additionally guards the allocation-free primitives.
- name: Build no_std + no_alloc (bare-metal target)
run: nix develop --command cargo build --no-default-features --lib --target thumbv7em-none-eabi --verbose

- name: Build no_std + alloc (bare-metal target)
run: nix develop --command cargo build --no-default-features --features alloc --lib --target thumbv7em-none-eabi --verbose

fuzz:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -99,6 +103,15 @@ jobs:
- name: Run clippy
run: nix develop --command cargo clippy --all-targets -- -D warnings

# The `#[cfg(feature = "alloc")]` gating means an unused import in a
# reduced feature set can only be caught by linting that set. Benches and
# integration targets are not feature-gated, so these are lib-scoped.
- name: Run clippy (no_std, no allocator)
run: nix develop --command cargo clippy --no-default-features --lib -- -D warnings

- name: Run clippy (no_std, alloc)
run: nix develop --command cargo clippy --no-default-features --features alloc --lib -- -D warnings

codegen:
runs-on: ubuntu-latest
steps:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - 2026-06-13
- Add allocation-free `encode_char` / `decode_byte` primitives, available without an allocator (e.g. for `no_std` embedded targets).
- Add an `alloc` feature gating the `Cow`-returning `encode`/`decode` API. `std` now implies `alloc`, so the default build is unchanged.
- **Breaking change** (only for the `default-features = false` case): `default-features = false` previously implied an allocator and kept the full API. It now selects the no-allocator tier; add `features = ["alloc"]` to restore the `no_std` + allocator behavior introduced in 1.4.0. The default build is unaffected.

## [1.4.0] - 2026-06-09
- Add `no_std` support. Disable the default `std` feature (`default-features = false`) to build without the standard library; the crate still requires an allocator. The only difference is that the `std::error::Error` impls for `EncodeError`/`DecodeError` are omitted. No changes for existing users.

Expand Down
13 changes: 8 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "yore"
version = "1.4.0"
version = "2.0.0"
authors = ["Andreas Liljeqvist <bonega@gmail.com>"]
edition = "2021"
categories = ["encoding"]
Expand All @@ -13,10 +13,13 @@ repository = "https://github.com/bonega/yore/"

[features]
default = ["std"]
# Enables `std::error::Error` impls for the error types.
# Disable (`--no-default-features`) to build as `no_std`; the crate still
# requires an allocator (it uses the `alloc` crate).
std = []
# Enables `std::error::Error` impls for the error types. Implies `alloc`.
std = ["alloc"]
# Enables the allocating API (the `Cow`-returning `encode`/`decode` family),
# pulling in the `alloc` crate. Disable (`--no-default-features`) to build for
# `no_std` targets without an allocator; only the allocation-free char
# primitives (`encode_char`, `decode_byte`) remain available.
alloc = []

[dependencies]

Expand Down
37 changes: 28 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A Rust library for decoding and encoding character sets based on OEM code pages.

[![yore at crates.io](https://img.shields.io/badge/crates.io-1.4.0-blue)](https://crates.io/crates/yore)
[![yore at crates.io](https://img.shields.io/badge/crates.io-2.0.0-blue)](https://crates.io/crates/yore)
[![yore at docs.rs](https://docs.rs/yore/badge.svg)](https://docs.rs/yore)

# Features
Expand All @@ -11,30 +11,49 @@ A Rust library for decoding and encoding character sets based on OEM code pages.
* Easy-to-use API
* Broad range of [supported code pages](#supported-code-pages)
* Handles code pages with redefined ASCII characters (<0x80), such as '٪' in CP864
* `no_std` support (requires an allocator) by disabling default features
* `no_std` support, with or without an allocator, via cargo features
* Allocation-free `encode_char` / `decode_byte` primitives for embedded use

# Usage

Add `yore` to your `Cargo.toml` file.

```toml
[dependencies]
yore = "1.4.0"
yore = "2.0.0"
```

## `no_std`

`yore` builds without the standard library when the default `std` feature is
disabled. It still requires an allocator, since the API returns owned `Cow`
buffers.
`yore` has three feature tiers, so it scales from std down to bare-metal targets
with no allocator:

| Cargo features | Environment | API |
| --- | --- | --- |
| `std` (default) | `std` | Full API + `std::error::Error` impls |
| `alloc` | `no_std` + allocator | Full API; `Error` impls omitted (`Display` stays) |
| *(none)* | `no_std`, no allocator | Allocation-free char primitives only |

The allocating `encode`/`decode` family returns owned `Cow` buffers and so
requires the `alloc` feature. Without it, only the allocation-free
`encode_char` / `decode_byte` primitives are available:

```toml
[dependencies]
yore = { version = "1.4.0", default-features = false }
# no_std with an allocator: keep the full Cow-returning API
yore = { version = "2.0.0", default-features = false, features = ["alloc"] }

# no_std without an allocator: char primitives only
yore = { version = "2.0.0", default-features = false }
```

The only difference from the default build is that the `std::error::Error` impls
for `EncodeError`/`DecodeError` are omitted (`Display` is always available).
```rust
use yore::code_pages::CP850;

// Encode/decode one character at a time, no allocation required.
assert_eq!(CP850.encode_char('A'), Some(b'A'));
assert_eq!(CP850.decode_byte(b'A'), 'A');
```

# Examples

Expand Down
60 changes: 58 additions & 2 deletions codegen/src/codegen_helper/templates/complete.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
#[cfg(feature = "alloc")]
use alloc::borrow::Cow;

use crate::{
decoder::{self, complete::decode_helper, CompleteEntry},
decoder::{self, CompleteEntry},
encoder::Encoder,
CodePage, DecodeError, EncodeError,
CodePage,
};

#[cfg(feature = "alloc")]
use crate::decoder::complete::decode_helper;

#[cfg(feature = "alloc")]
use crate::{DecodeError, EncodeError};

#[derive(Copy, Clone)]
pub struct CODERSTRUCT;

Expand All @@ -18,11 +26,35 @@ impl CODERSTRUCT {
///
/// assert_eq!(CODERSTRUCT.decode(&[116, 101, 120, 116]), "text");
/// ```
#[cfg(feature = "alloc")]
#[inline(always)]
pub fn decode(self, bytes: &[u8]) -> Cow<'_, str> {
decode_helper(&DECODE_TABLE, bytes)
}

/// Decode a single CODERSTRUCT byte into its character.
///
/// Allocation-free, so available without the `alloc` feature. CODERSTRUCT
/// is a complete codepage (every byte maps to a character), so this is
/// infallible and returns `char` directly.
///
/// # Examples
///
/// ```
/// use yore::code_pages::CODERSTRUCT;
///
/// assert_eq!(CODERSTRUCT.decode_byte(b't'), 't');
/// ```
#[inline(always)]
pub fn decode_byte(self, b: u8) -> char {
let entry = DECODE_TABLE[b as usize];
// SAFETY: table contents are valid UTF-8 by construction.
unsafe { core::str::from_utf8_unchecked(&entry.buf[..entry.len as usize]) }
.chars()
.next()
.unwrap()
}

/// Encode UTF-8 string into CODERSTRUCT byte-encoding
///
/// Undefined characters will result in [`EncodeError`]
Expand All @@ -36,6 +68,7 @@ impl CODERSTRUCT {
/// assert_eq!(CODERSTRUCT.encode("text").unwrap(), vec![116, 101, 120, 116]);
/// assert!(matches!(CODERSTRUCT.encode("text 🦀"), EncodeError));
/// ```
#[cfg(feature = "alloc")]
#[inline(always)]
pub fn encode(self, s: &str) -> Result<Cow<'_, [u8]>, EncodeError> {
self.encode_helper(s, None)
Expand All @@ -52,12 +85,35 @@ impl CODERSTRUCT {
///
/// assert_eq!(CODERSTRUCT.encode_lossy("text 🦀", 168), vec![116, 101, 120, 116, 32, 168]);
/// ```
#[cfg(feature = "alloc")]
#[inline(always)]
pub fn encode_lossy(self, s: &str, fallback: u8) -> Cow<'_, [u8]> {
self.encode_helper(s, Some(fallback)).unwrap()
}

/// Encode a single Unicode `char` into its CODERSTRUCT byte.
///
/// Returns `None` if the character has no mapping. Allocation-free, so
/// available without the `alloc` feature; compose with `s.chars()` for
/// streaming use:
///
/// ```
/// use yore::code_pages::CODERSTRUCT;
///
/// let s = "text";
/// let bytes: Vec<u8> = s.chars().map(|c| CODERSTRUCT.encode_char(c).unwrap()).collect();
/// assert_eq!(bytes, vec![116, 101, 120, 116]);
/// ```
#[inline]
pub fn encode_char(self, c: char) -> Option<u8> {
let mut buf = [0u8; 4];
let utf8 = c.encode_utf8(&mut buf).as_bytes();
let mut slice: &[u8] = utf8;
self.encode_grapheme(&mut slice)
}
}
impl CodePage for CODERSTRUCT {
#[cfg(feature = "alloc")]
#[inline(always)]
fn decode<'a>(&self, bytes: &'a [u8]) -> Result<Cow<'a, str>, DecodeError> {
Ok((*self).decode(bytes))
Expand Down
68 changes: 61 additions & 7 deletions codegen/src/codegen_helper/templates/incomplete.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
#[cfg(feature = "alloc")]
use alloc::borrow::Cow;

use crate::{
decoder::{
self,
complete::decode_helper as decode_helper_lossy,
incomplete::decode_helper,
CompleteEntry, IncompleteEntry, IncompleteLen,
},
decoder::{self, IncompleteEntry, IncompleteLen},
encoder::Encoder,
CodePage, DecodeError, EncodeError,
CodePage,
};

#[cfg(feature = "alloc")]
use crate::decoder::{
complete::decode_helper as decode_helper_lossy, incomplete::decode_helper, CompleteEntry,
};

#[cfg(feature = "alloc")]
use crate::{DecodeError, EncodeError};

impl CODERSTRUCT {
/// Decode CODERSTRUCT byte-encoding into UTF-8 string
///
Expand All @@ -23,6 +27,7 @@ impl CODERSTRUCT {
///
/// assert_eq!(CODERSTRUCT.decode(&[116, 101, 120, 116]).unwrap(), "text");
/// ```
#[cfg(feature = "alloc")]
#[inline(always)]
pub fn decode(self, bytes: &[u8]) -> Result<Cow<'_, str>, DecodeError> {
decode_helper(&DECODE_TABLE, bytes, None)
Expand All @@ -39,6 +44,7 @@ impl CODERSTRUCT {
///
/// assert_eq!(CODERSTRUCT.decode_lossy(&[116, 101, 120, 116]), "text");
/// ```
#[cfg(feature = "alloc")]
#[inline(always)]
pub fn decode_lossy(self, bytes: &[u8]) -> Cow<'_, str> {
decode_helper_lossy(&DECODE_TABLE_LOSSY, bytes)
Expand All @@ -58,11 +64,32 @@ impl CODERSTRUCT {
///
/// assert_eq!(CODERSTRUCT.decode_lossy_fallback(&[116, 101, 120, 116], '�'), "text");
/// ```
#[cfg(feature = "alloc")]
#[inline(always)]
pub fn decode_lossy_fallback(self, bytes: &[u8], fallback: char) -> Cow<'_, str> {
decode_helper(&DECODE_TABLE, bytes, Some(fallback)).unwrap()
}

/// Decode a single CODERSTRUCT byte into its character.
///
/// Returns `None` for an undefined codepoint. Allocation-free, so available
/// without the `alloc` feature.
///
/// # Examples
///
/// ```
/// use yore::code_pages::CODERSTRUCT;
///
/// assert_eq!(CODERSTRUCT.decode_byte(b't'), Some('t'));
/// ```
#[inline(always)]
pub fn decode_byte(self, b: u8) -> Option<char> {
let entry = DECODE_TABLE[b as usize]?;
// SAFETY: table contents are valid UTF-8 by construction.
let s = unsafe { core::str::from_utf8_unchecked(&entry.buf[..entry.len as usize]) };
s.chars().next()
}

/// Encode UTF-8 string into CODERSTRUCT byte-encoding
///
/// Undefined characters will result in [`EncodeError`]
Expand All @@ -76,6 +103,7 @@ impl CODERSTRUCT {
/// assert_eq!(CODERSTRUCT.encode("text").unwrap(), vec![116, 101, 120, 116]);
/// assert!(matches!(CODERSTRUCT.encode("text 🦀"), EncodeError));
/// ```
#[cfg(feature = "alloc")]
#[inline(always)]
pub fn encode(self, s: &str) -> Result<Cow<'_, [u8]>, EncodeError> {
self.encode_helper(s, None)
Expand All @@ -92,21 +120,45 @@ impl CODERSTRUCT {
///
/// assert_eq!(CODERSTRUCT.encode_lossy("text 🦀", 168), vec![116, 101, 120, 116, 32, 168]);
/// ```
#[cfg(feature = "alloc")]
#[inline(always)]
pub fn encode_lossy(self, s: &str, fallback: u8) -> Cow<'_, [u8]> {
self.encode_helper(s, Some(fallback)).unwrap()
}

/// Encode a single Unicode `char` into its CODERSTRUCT byte.
///
/// Returns `None` if the character has no mapping. Allocation-free, so
/// available without the `alloc` feature; compose with `s.chars()` for
/// streaming use:
///
/// ```
/// use yore::code_pages::CODERSTRUCT;
///
/// let s = "text";
/// let bytes: Vec<u8> = s.chars().map(|c| CODERSTRUCT.encode_char(c).unwrap()).collect();
/// assert_eq!(bytes, vec![116, 101, 120, 116]);
/// ```
#[inline]
pub fn encode_char(self, c: char) -> Option<u8> {
let mut buf = [0u8; 4];
let utf8 = c.encode_utf8(&mut buf).as_bytes();
let mut slice: &[u8] = utf8;
self.encode_grapheme(&mut slice)
}
}

#[derive(Copy, Clone)]
pub struct CODERSTRUCT;

impl CodePage for CODERSTRUCT {
#[cfg(feature = "alloc")]
#[inline(always)]
fn decode<'a>(&self, bytes: &'a [u8]) -> Result<Cow<'a, str>, DecodeError> {
(*self).decode(bytes)
}

#[cfg(feature = "alloc")]
#[inline(always)]
fn decode_lossy<'a>(&self, bytes: &'a [u8]) -> Cow<'a, str> {
(*self).decode_lossy(bytes)
Expand All @@ -115,11 +167,13 @@ impl CodePage for CODERSTRUCT {
/// Note that the `fallback` char should be less than 4 bytes in UTF8.
/// 4 bytes UTF8 will panic because of assertion.
/// Refrain from using emojis as fallback
#[cfg(feature = "alloc")]
#[inline(always)]
fn decode_lossy_fallback<'a>(&self, bytes: &'a [u8], fallback: char) -> Cow<'a, str> {
(*self).decode_lossy_fallback(bytes, fallback)
}
}

const DECODE_TABLE: decoder::incomplete::Table = PLACEHOLDER_TABLE;
#[cfg(feature = "alloc")]
const DECODE_TABLE_LOSSY: decoder::complete::Table = PLACEHOLDER_LOSSY_TABLE;
Loading
Loading