use alloc::borrow::Cow;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
#[derive(Debug, Clone, Default)]
pub struct Strings<'a> {
blob: Cow<'a, [u8]>,
starts: Vec<u32>,
}
impl<'a> Strings<'a> {
#[must_use]
pub fn new<S: AsRef<str>>(names: impl IntoIterator<Item = S>) -> Strings<'static> {
let mut blob = Vec::new();
let mut starts = vec![0u32];
for name in names {
blob.extend_from_slice(name.as_ref().as_bytes());
starts.push(blob.len() as u32);
}
Strings {
blob: Cow::Owned(blob),
starts,
}
}
pub fn borrowed(blob: &'a [u8], starts: Vec<u32>) -> Result<Self, BadTable> {
if starts.is_empty() {
return Err(BadTable::NoTerminator);
}
if starts[0] != 0 {
return Err(BadTable::NoTerminator);
}
for pair in starts.windows(2) {
let (from, to) = (pair[0] as usize, pair[1] as usize);
if to < from || to > blob.len() {
return Err(BadTable::SpanOutOfRange { from, to });
}
core::str::from_utf8(&blob[from..to]).map_err(|_| BadTable::NotUtf8 { at: from })?;
}
if *starts.last().expect("checked") as usize != blob.len() {
return Err(BadTable::TrailingBytes);
}
Ok(Self {
blob: Cow::Borrowed(blob),
starts,
})
}
#[must_use]
pub fn len(&self) -> usize {
self.starts.len().saturating_sub(1)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn get(&self, index: u32) -> Option<&str> {
let from = *self.starts.get(index as usize)? as usize;
let to = *self.starts.get(index as usize + 1)? as usize;
core::str::from_utf8(self.blob.get(from..to)?).ok()
}
#[must_use]
pub fn blob(&self) -> &[u8] {
&self.blob
}
#[must_use]
pub fn starts(&self) -> &[u32] {
&self.starts
}
pub fn iter(&self) -> impl Iterator<Item = &str> + '_ {
(0..self.len() as u32).filter_map(|index| self.get(index))
}
#[must_use]
pub fn into_owned(self) -> Strings<'static> {
Strings {
blob: Cow::Owned(self.blob.into_owned()),
starts: self.starts,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BadTable {
NoTerminator,
SpanOutOfRange {
from: usize,
to: usize,
},
NotUtf8 {
at: usize,
},
TrailingBytes,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_come_back_in_order() {
let table = Strings::new(["alpha", "b", "", "gamma"]);
assert_eq!(table.len(), 4);
assert_eq!(table.get(0), Some("alpha"));
assert_eq!(table.get(1), Some("b"));
assert_eq!(table.get(2), Some(""), "an empty name is still a name");
assert_eq!(table.get(3), Some("gamma"));
assert_eq!(table.get(4), None);
}
#[test]
fn a_table_survives_being_taken_apart_and_borrowed_back() {
let owned = Strings::new(["one", "two", "three"]);
let blob = owned.blob().to_vec();
let borrowed = Strings::borrowed(&blob, owned.starts().to_vec()).expect("sound");
assert_eq!(borrowed.iter().collect::<Vec<_>>(), ["one", "two", "three"]);
}
#[test]
fn an_empty_table_is_sound() {
let table = Strings::new(Vec::<&str>::new());
assert!(table.is_empty());
assert_eq!(table.get(0), None);
let borrowed = Strings::borrowed(&[], vec![0]).expect("sound");
assert!(borrowed.is_empty());
}
#[test]
fn spans_that_do_not_fit_the_blob_are_refused() {
assert_eq!(
Strings::borrowed(b"abc", vec![]).err(),
Some(BadTable::NoTerminator),
);
assert_eq!(
Strings::borrowed(b"abc", vec![1, 3]).err(),
Some(BadTable::NoTerminator),
);
assert!(matches!(
Strings::borrowed(b"abc", vec![0, 99]),
Err(BadTable::SpanOutOfRange { .. }),
));
assert!(matches!(
Strings::borrowed(b"abc", vec![0, 2, 1]),
Err(BadTable::SpanOutOfRange { .. }),
));
assert_eq!(
Strings::borrowed(b"abcd", vec![0, 3]).err(),
Some(BadTable::TrailingBytes),
);
}
#[test]
fn a_name_that_is_not_utf8_is_refused() {
assert!(matches!(
Strings::borrowed(&[0xff, 0xfe], vec![0, 2]),
Err(BadTable::NotUtf8 { .. }),
));
}
#[test]
fn borrowing_points_into_the_caller_s_buffer() {
let blob = b"alpha-beta".to_vec();
let table = Strings::borrowed(&blob, vec![0, 5, 6, 10]).expect("sound");
let name = table.get(0).expect("present");
assert_eq!(name, "alpha");
assert!(
name.as_ptr() >= blob.as_ptr()
&& (name.as_ptr() as usize) < blob.as_ptr() as usize + blob.len(),
"a borrowed name must point into the blob, not into a copy",
);
}
}