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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ on:
inputs:
release:
type: boolean
description: 'Create a GitHub Release?'
description: "Create a GitHub Release?"
default: false
required: false
publish:
type: boolean
description: 'Publish to crates.io?'
description: "Publish to crates.io?"
default: false
required: false
version:
type: string
description: 'Version (if $GITHUB_REF is not a tag)'
default: ''
description: "Version (if $GITHUB_REF is not a tag)"
default: ""
required: false
env:
CARGO_TERM_COLOR: always
Expand Down
4 changes: 2 additions & 2 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[toolchain]
channel = "stable"
channel = "nightly"
components = ["rustfmt", "clippy", "cargo"]
profile = "minimal"
profile = "minimal"
90 changes: 87 additions & 3 deletions src/cow_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use core::mem::transmute_copy;
use core::ops::Deref;
use core::ops::DerefMut;
use core::str;
use core::str::FromStr;

use crate::inline_str::*;

Expand Down Expand Up @@ -143,9 +144,7 @@ impl<'i> CowStr<'i> {
self.len() == 0
}

/// Converts the `CowStr` into an owned `String`, cloning the data if
/// necessary.
#[deprecated(since = "0.4.0", note = "use `into_string` instead")]
#[deprecated(since = "0.2.0", note = "use `into_string` instead")]
#[inline(always)]
pub fn into_owned(self) -> String {
match self {
Expand All @@ -155,6 +154,8 @@ impl<'i> CowStr<'i> {
}
}

/// Converts the `CowStr` into an owned `String`, cloning the data if
/// necessary.
#[inline(always)]
pub fn into_string(self) -> String {
match self {
Expand All @@ -165,6 +166,18 @@ impl<'i> CowStr<'i> {
}
}

impl<'i> FromStr for CowStr<'i> {
type Err = ();

#[inline(always)]
fn from_str(s: &str) -> Result<Self, Self::Err> {

Copilot AI Dec 24, 2025

Copy link

Choose a reason for hiding this comment

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

The FromStr implementation returns Result<Self, ()> which provides no error information. Since this implementation never fails (it falls back to Owned when inlining fails), consider returning Self directly by implementing From<&str> instead, or use a meaningful error type if error cases are anticipated.

Copilot uses AI. Check for mistakes.
match InlineStr::try_from(s) {
Ok(inline) => Ok(CowStr::Inlined(inline)),
Err(_) => Ok(CowStr::Owned(s.to_string().into_boxed_str())),
}
}
}

impl<'i> Display for CowStr<'i> {
#[inline(always)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand Down Expand Up @@ -396,6 +409,77 @@ impl<'i> From<CowStr<'i>> for String {
}
}

#[cfg(not(feature = "is_variant"))]
impl<'i> CowStr<'i> {
/// Returns `true` if the `CowStr` is the `Owned` variant.
#[inline(always)]
pub const fn is_owned(&self) -> bool {
matches!(self, CowStr::Owned(_))
}

/// Returns `true` if the `CowStr` is the `Inlined` variant.
#[inline(always)]
pub const fn is_inlined(&self) -> bool {
matches!(self, CowStr::Inlined(_))
}

/// Returns `true` if the `CowStr` is the `Borrowed` variant.
#[inline(always)]
pub const fn is_borrowed(&self) -> bool {
matches!(self, CowStr::Borrowed(_))
}
}

impl CowStr<'_> {
/// Attempts to create an inline `CowStr` from a value that can be converted
/// to a string slice via an `AsRef<str>` impl.
///
/// Returns an error if the string is too long to be inlined.
#[inline(always)]
pub fn try_inline<'i, T: 'i + AsRef<str>>(
s: T,
) -> Result<CowStr<'i>, StringTooLongError> {
let inline = InlineStr::try_from(s.as_ref())?;
Ok(CowStr::Inlined(inline))
}

/// Creates an inline `CowStr` from a value that can be converted to a string
/// slice via an `AsRef<str>` impl, panicking if the string is too long to be
/// inlined.
///
/// # Panics
///
/// Panics if the string length exceeds [`MAX_INLINE_STR_LEN`].
#[inline(always)]
pub fn inline<'i, T: 'i + AsRef<str>>(s: T) -> CowStr<'i> {
let inline =
InlineStr::try_from(s.as_ref()).expect("String too long to inline!");
CowStr::Inlined(inline)
}

/// Forcibly creates an inline `CowStr` from a given value that can be
/// converted to a string slice via an `AsRef<str>` impl, truncating it if
/// necessary to fit within the maximum inline length.
#[inline(always)]
pub fn force_inline<'i, T: 'i + AsRef<str>>(s: T) -> CowStr<'i> {
let src = s.as_ref().as_bytes();
let mut len = src.len();
if len > MAX_INLINE_STR_LEN {
len = MAX_INLINE_STR_LEN;
}
Comment on lines +465 to +469

Copilot AI Dec 24, 2025

Copy link

Choose a reason for hiding this comment

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

Truncating UTF-8 strings at arbitrary byte boundaries can split multi-byte characters, resulting in invalid UTF-8. Use character boundary checking (e.g., s.as_ref().char_indices()) to find a safe truncation point instead of slicing at MAX_INLINE_STR_LEN bytes.

Suggested change
let src = s.as_ref().as_bytes();
let mut len = src.len();
if len > MAX_INLINE_STR_LEN {
len = MAX_INLINE_STR_LEN;
}
let s_ref = s.as_ref();
let mut len = s_ref.len();
if len > MAX_INLINE_STR_LEN {
// Truncate at a valid UTF-8 character boundary so we don't split
// multi-byte characters.
let mut safe_len = 0usize;
for (idx, ch) in s_ref.char_indices() {
let ch_end = idx + ch.len_utf8();
if ch_end > MAX_INLINE_STR_LEN {
break;
}
safe_len = ch_end;
}
len = safe_len;
}
let src = s_ref.as_bytes();

Copilot uses AI. Check for mistakes.
let mut buf = [0u8; MAX_INLINE_STR_LEN];
buf[..len].copy_from_slice(&src[..len]);
let len = len as u8;
CowStr::Inlined(InlineStr { buf, len })
}

/// Creates an inline `CowStr` from a single character.
#[inline(always)]
pub fn from_char(c: char) -> CowStr<'static> {
CowStr::Inlined(c.into())
}
}

#[cfg(feature = "serde")]
mod serde_impl {
use core::fmt;
Expand Down
Loading