#![cfg_attr(all(not(test)), no_std)]
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
#![warn(clippy::cargo)]
#![warn(missing_docs)]
unsafe fn from_utf8_unchecked(input: &[u8]) -> &str {
debug_assert!(
core::str::from_utf8(input).is_ok(),
"{:?} was invalid UTF-8",
input
);
core::str::from_utf8_unchecked(input)
}
unsafe fn from_utf8_unchecked_mut(input: &mut [u8]) -> &mut str {
debug_assert!(
core::str::from_utf8(input).is_ok(),
"{:?} was invalid UTF-8",
input
);
core::str::from_utf8_unchecked_mut(input)
}
#[derive(Debug)]
pub struct MapInPlace<'a> {
buf: &'a mut [u8],
mapped_head: usize,
unmapped_head: usize,
}
#[inline]
#[allow(clippy::cast_possible_wrap)]
const fn is_char_start(byte: u8) -> bool {
(byte as i8) >= -0x40
}
#[derive(Debug)]
pub struct NoCapacityError(());
const PARTIAL_ZERO_SIZE: usize = 32;
impl<'a> MapInPlace<'a> {
pub fn new(s: &'a mut str) -> Self {
let buf = unsafe { s.as_bytes_mut() };
MapInPlace {
buf,
mapped_head: 0,
unmapped_head: 0,
}
}
#[must_use]
pub fn mapped(&self) -> &str {
debug_assert!(self.buf.get(0..self.mapped_head).is_some());
let bytes = unsafe { self.buf.get_unchecked(0..self.mapped_head) };
unsafe { from_utf8_unchecked(bytes) }
}
#[must_use]
pub fn into_mapped(self) -> &'a mut str {
let mapped_head = self.mapped_head;
&mut self.into_all()[0..mapped_head]
}
#[must_use]
pub fn unmapped(&self) -> &str {
&self.all()[self.unmapped_head..]
}
#[must_use]
pub fn into_unmapped(self) -> &'a mut str {
let unmapped_head = self.unmapped_head;
&mut self.into_all()[unmapped_head..]
}
#[must_use]
fn all(&self) -> &str {
unsafe { from_utf8_unchecked(&self.buf[..]) }
}
#[must_use]
fn into_all(self) -> &'a mut str {
unsafe { from_utf8_unchecked_mut(self.buf) }
}
pub fn push(&mut self, ch: char) -> Result<(), NoCapacityError> {
let mut tempbuf = [0_u8; 4_usize];
let sbytes = ch.encode_utf8(&mut tempbuf);
self.push_str(sbytes)?;
Ok(())
}
pub fn push_str(&mut self, s: &str) -> Result<(), NoCapacityError> {
let bytes = s.as_bytes();
if self.buf.len() < self.mapped_head + bytes.len() {
return Err(NoCapacityError(()));
}
if self.unmapped_head < self.mapped_head + bytes.len() {
return Err(NoCapacityError(()));
}
self.buf[self.mapped_head..self.mapped_head + bytes.len()].copy_from_slice(bytes);
self.mapped_head += bytes.len();
debug_assert!(self.mapped_head <= self.unmapped_head);
let area_to_zero = &mut self.buf[self.mapped_head..self.unmapped_head];
if area_to_zero.len() > PARTIAL_ZERO_SIZE {
for byte in area_to_zero {
if is_char_start(*byte) {
break;
}
*byte = 0;
}
} else {
area_to_zero.fill(0);
}
Ok(())
}
pub fn pop(&mut self) -> Option<char> {
self.pop_chars(1)
.map(|x| x.chars().next().expect("pop_chars did not pop a char"))
}
pub fn pop_chars(&mut self, n: usize) -> Option<&str> {
if n == 0 {
return None;
}
let (idx, c) = self.unmapped().char_indices().nth(n - 1)?;
let to_take = idx + c.len_utf8();
let s = &self.buf[self.unmapped_head..self.unmapped_head + to_take];
self.unmapped_head += to_take;
unsafe { Some(from_utf8_unchecked(s)) }
}
}
#[cfg(doctest)]
mod test_readme {
macro_rules! external_doc_test {
($x:expr) => {
#[doc = $x]
extern "C" {}
};
}
external_doc_test!(include_str!("../README.md"));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cannot_remove_from_end() {
let mut initial = "㉉".to_string();
let mut mapper = MapInPlace::new(&mut initial);
mapper.pop_chars(3);
}
}