Skip to content

String insertion #584

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 14, 2025
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added

- Added `String::insert` and `String::insert_str`.

### Changed

- `bytes::BufMut` is now implemented on `VecInner`.
Expand Down
222 changes: 222 additions & 0 deletions src/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,129 @@ impl<LenT: LenType, S: StringStorage + ?Sized> StringInner<LenT, S> {
pub fn clear(&mut self) {
self.vec.clear();
}

/// Inserts a character into this `String` at a byte position.
///
/// This is an *O*(*n*) operation as it requires copying every element in the
/// buffer.
///
/// # Panics
///
/// Panics if `idx` is larger than the `String`'s length, or if it does not
/// lie on a [`char`] boundary.
///
/// # Examples
///
/// ```
/// use heapless::String;
///
/// let mut s: String<4> = String::new();
///
/// s.insert(0, 'f').unwrap();
/// s.insert(1, 'o').unwrap();
/// s.insert(2, 'o').unwrap();
///
/// assert_eq!("foo", s);
/// # Ok::<(), heapless::CapacityError>(())
/// ```
#[inline]
pub fn insert(&mut self, idx: usize, ch: char) -> Result<(), CapacityError> {
assert!(self.is_char_boundary(idx), "index must be a char boundary");

let len = self.len();
let ch_len = ch.len_utf8();

// Check if there is enough capacity
if len + ch_len > self.capacity() {
return Err(CapacityError);
}

// SAFETY: Move the bytes starting from `idx` to their new location `ch_len`
// bytes ahead. This is safe because we checked `len + ch_len` does not
// exceed the capacity and `idx` is a char boundary.
unsafe {
let ptr = self.vec.as_mut_ptr();
core::ptr::copy(ptr.add(idx), ptr.add(idx + ch_len), len - idx);
}

// SAFETY: Encode the character into the vacated region if `idx != len`,
// or into the uninitialized spare capacity otherwise. This is safe
// because `is_char_boundary` checks that `idx <= len`, and we checked that
// `(idx + ch_len)` does not exceed the capacity.
unsafe {
let buf = core::slice::from_raw_parts_mut(self.vec.as_mut_ptr().add(idx), ch_len);
ch.encode_utf8(buf);
}

// SAFETY: Update the length to include the newly added bytes. This is
// safe because we checked that `len + ch_len` does not exceed the capacity.
unsafe {
self.vec.set_len(len + ch_len);
}

Ok(())
}

/// Inserts a string slice into this `String` at a byte position.
///
/// This is an *O*(*n*) operation as it requires copying every element in the
/// buffer.
///
/// # Panics
///
/// Panics if `idx` is larger than the `String`'s length, or if it does not
/// lie on a [`char`] boundary.
///
/// # Examples
///
/// ```
/// use heapless::String;
///
/// let mut s: String<8> = String::try_from("bar")?;
///
/// s.insert_str(0, "foo")?;
///
/// assert_eq!("foobar", s);
/// # Ok::<(), heapless::CapacityError>(())
/// ```
#[inline]
pub fn insert_str(&mut self, idx: usize, string: &str) -> Result<(), CapacityError> {
assert!(self.is_char_boundary(idx), "index must be a char boundary");

let len = self.len();
let string_len = string.len();

// Check if there is enough capacity
if len + string_len > self.capacity() {
return Err(CapacityError);
}

// SAFETY: Move the bytes starting from `idx` to their new location
// `string_len` bytes ahead. This is safe because we checked there is
// sufficient capacity, and `idx` is a char boundary.
unsafe {
let ptr = self.vec.as_mut_ptr();
core::ptr::copy(ptr.add(idx), ptr.add(idx + string_len), len - idx);
}

// SAFETY: Copy the new string slice into the vacated region if `idx != len`,
// or into the uninitialized spare capacity otherwise. The borrow checker
// ensures that the source and destination do not overlap.
unsafe {
core::ptr::copy_nonoverlapping(
string.as_ptr(),
self.vec.as_mut_ptr().add(idx),
string_len,
);
}

// SAFETY: Update the length to include the newly added bytes.
unsafe {
self.vec.set_len(len + string_len);
}

Ok(())
}
}

impl<LenT: LenType, const N: usize> Default for String<N, LenT> {
Expand Down Expand Up @@ -1240,4 +1363,103 @@ mod tests {
let formatted = format!(2; "123");
assert_eq!(formatted, Err(core::fmt::Error));
}

#[test]
fn insert() {
let mut s: String<6> = String::try_from("123").unwrap();
assert!(s.insert(0, 'a').is_ok());
assert_eq!(s, "a123");

assert!(s.insert(2, 'b').is_ok());
assert_eq!(s, "a1b23");

assert!(s.insert(s.len(), '4').is_ok());
assert_eq!(s, "a1b234");

assert_eq!(s.len(), 6);
assert!(s.insert(0, 'd').is_err());
assert_eq!(s, "a1b234");
}

#[test]
fn insert_unicode() {
let mut s: String<21> = String::try_from("ĝėēƶ").unwrap();
let idx = s.find("ė").unwrap();

assert!(s.insert(idx, '🦀').is_ok());
assert_eq!(s, "ĝ🦀ėēƶ");

s.insert(s.len(), '🦀').unwrap();
assert_eq!(s, "ĝ🦀ėēƶ🦀");

s.insert(0, '🦀').unwrap();
assert_eq!(s, "🦀ĝ🦀ėēƶ🦀");

assert_eq!(s.len(), 20);
assert_eq!('ƶ'.len_utf8(), 2);
assert!(s.insert(0, 'ƶ').is_err());
assert_eq!(s, "🦀ĝ🦀ėēƶ🦀");
}

#[test]
#[should_panic = "index must be a char boundary"]
fn insert_at_non_char_boundary_panics() {
let mut s: String<8> = String::try_from("é").unwrap();
_ = s.insert(1, 'a');
}

#[test]
#[should_panic = "index must be a char boundary"]
fn insert_beyond_length_panics() {
let mut s: String<8> = String::try_from("a").unwrap();
_ = s.insert(2, 'a');
}

#[test]
fn insert_str() {
let mut s: String<14> = String::try_from("bar").unwrap();
assert!(s.insert_str(0, "foo").is_ok());
assert_eq!(s, "foobar");

assert!(s.insert_str(3, "baz").is_ok());
assert_eq!(s, "foobazbar");

assert!(s.insert_str(s.len(), "end").is_ok());
assert_eq!(s, "foobazbarend");

assert_eq!(s.len(), 12);
assert!(s.insert_str(0, "def").is_err());
assert_eq!(s, "foobazbarend");
}

#[test]
fn insert_str_unicode() {
let mut s: String<20> = String::try_from("Héllô").unwrap();
let idx = s.find("lô").unwrap();

assert!(s.insert_str(idx, "p, í'm ").is_ok());
assert_eq!(s, "Hélp, í'm lô");

assert!(s.insert_str(s.len(), "st").is_ok());
assert_eq!(s, "Hélp, í'm lôst");

assert_eq!(s.len(), 17);
assert_eq!("🦀".len(), 4);
assert!(s.insert_str(0, "🦀").is_err());
assert_eq!(s, "Hélp, í'm lôst");
}

#[test]
#[should_panic = "index must be a char boundary"]
fn insert_str_at_non_char_boundary_panics() {
let mut s: String<8> = String::try_from("é").unwrap();
_ = s.insert_str(1, "a");
}

#[test]
#[should_panic = "index must be a char boundary"]
fn insert_str_beyond_length_panics() {
let mut s: String<8> = String::try_from("a").unwrap();
_ = s.insert_str(2, "a");
}
}