use core::fmt;
use core::ops::Index;
use core::str;
use scroll::{Pread, ctx};
if_alloc! {
use crate::error;
use crate::options::Permissive;
use alloc::vec::Vec;
}
pub struct Strtab<'a> {
delim: ctx::StrCtx,
bytes: &'a [u8],
#[cfg(feature = "alloc")]
strings: Vec<(usize, &'a str)>,
}
#[inline(always)]
fn get_str(offset: usize, bytes: &[u8], delim: ctx::StrCtx) -> scroll::Result<&str> {
bytes.pread_with::<&str>(offset, delim)
}
#[inline(always)]
#[cfg(feature = "alloc")]
fn get_str_with_opts(
offset: usize,
bytes: &[u8],
delim: ctx::StrCtx,
permissive: bool,
) -> scroll::Result<&str> {
bytes
.pread_with::<&str>(offset, delim)
.or_permissive_and_default(permissive, "Invalid UTF-8 in string table")
}
impl<'a> Strtab<'a> {
pub fn new(bytes: &'a [u8], delim: u8) -> Self {
Self::from_slice_unparsed(bytes, 0, bytes.len(), delim)
}
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn from_slice_unparsed(bytes: &'a [u8], offset: usize, len: usize, delim: u8) -> Self {
Self {
delim: ctx::StrCtx::Delimiter(delim),
bytes: &bytes[offset..offset + len],
#[cfg(feature = "alloc")]
strings: Vec::new(),
}
}
pub fn get_unsafe(&self, offset: usize) -> Option<&'a str> {
if offset >= self.bytes.len() {
None
} else {
Some(get_str(offset, self.bytes, self.delim).unwrap())
}
}
#[cfg(feature = "alloc")]
pub fn parse(bytes: &'a [u8], offset: usize, len: usize, delim: u8) -> error::Result<Self> {
Self::parse_with_opts(
bytes,
offset,
len,
delim,
&crate::options::ParseOptions::default(),
)
}
#[cfg(feature = "alloc")]
pub(crate) fn parse_with_opts(
bytes: &'a [u8],
offset: usize,
len: usize,
delim: u8,
opts: &crate::options::ParseOptions,
) -> error::Result<Self> {
let (end, overflow) = offset.overflowing_add(len);
if offset >= bytes.len() {
#[cfg(feature = "alloc")]
return Err(error::Error::Malformed(format!(
"String table offset ({}) is beyond file boundary ({})",
offset,
bytes.len()
)))
.or_permissive_and_value(
opts.parse_mode.is_permissive(),
"String table offset is beyond file boundary, returning empty string table",
Self {
delim: ctx::StrCtx::Delimiter(delim),
bytes: &[],
strings: Vec::new(),
},
);
#[cfg(not(feature = "alloc"))]
return Err(scroll::Error::BadOffset(offset).into()).or_permissive_and_value(
opts.parse_mode.is_permissive(),
"String table offset is beyond file boundary",
Self {
delim: ctx::StrCtx::Delimiter(delim),
bytes: &[],
},
);
}
let actual_len = if overflow || end > bytes.len() {
#[cfg(feature = "alloc")]
let err = Err(error::Error::Malformed(format!(
"Strtable size ({}) + offset ({}) is out of bounds for {} #bytes. Overflowed: {}",
len,
offset,
bytes.len(),
overflow
)));
#[cfg(not(feature = "alloc"))]
let err = Err(scroll::Error::BadOffset(offset).into());
err.or_permissive_and_then(
opts.parse_mode.is_permissive(),
"String table extends beyond file boundary, truncating",
|| bytes.len() - offset,
)?
} else {
len
};
let mut result = Self::from_slice_unparsed(bytes, offset, actual_len, delim);
let mut i = 0;
while i < result.bytes.len() {
let string = get_str_with_opts(
i,
result.bytes,
result.delim,
opts.parse_mode.is_permissive(),
)?;
result.strings.push((i, string));
i += string.len() + 1;
}
Ok(result)
}
#[cfg(feature = "alloc")]
pub fn new_preparsed(bytes: &'a [u8], delim: u8) -> error::Result<Self> {
Self::parse(bytes, 0, bytes.len(), delim)
}
#[cfg(feature = "alloc")]
pub fn to_vec(&self) -> error::Result<Vec<&'a str>> {
if self.strings.is_empty() {
let mut result = Vec::new();
let mut i = 0;
while i < self.bytes.len() {
let string = get_str(i, self.bytes, self.delim)?;
result.push(string);
i += string.len() + 1;
}
return Ok(result);
}
Ok(self.strings.iter().map(|&(_key, value)| value).collect())
}
#[cfg(feature = "alloc")]
pub fn get_at(&self, offset: usize) -> Option<&'a str> {
match self
.strings
.binary_search_by_key(&offset, |&(key, _value)| key)
{
Ok(index) => Some(self.strings[index].1),
Err(index) => {
if index == 0 {
return None;
}
let (string_begin_offset, entire_string) = self.strings[index - 1];
entire_string.get(offset - string_begin_offset..)
}
}
}
#[deprecated(since = "0.4.2", note = "Use from_slice_unparsed() instead")]
pub unsafe fn from_raw(ptr: *const u8, len: usize, delim: u8) -> Strtab<'a> {
unsafe { Self::from_slice_unparsed(core::slice::from_raw_parts(ptr, len), 0, len, delim) }
}
#[deprecated(since = "0.4.2", note = "Bad performance, use get_at() instead")]
#[cfg(feature = "alloc")]
pub fn get(&self, offset: usize) -> Option<error::Result<&'a str>> {
if offset >= self.bytes.len() {
None
} else {
Some(get_str(offset, self.bytes, self.delim).map_err(core::convert::Into::into))
}
}
}
impl<'a> fmt::Debug for Strtab<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Strtab")
.field("delim", &self.delim)
.field("bytes", &str::from_utf8(self.bytes))
.finish()
}
}
impl<'a> Default for Strtab<'a> {
fn default() -> Self {
Self {
delim: ctx::StrCtx::default(),
bytes: &[],
#[cfg(feature = "alloc")]
strings: Vec::new(),
}
}
}
impl<'a> Index<usize> for Strtab<'a> {
type Output = str;
#[inline(always)]
fn index(&self, offset: usize) -> &Self::Output {
get_str(offset, self.bytes, self.delim).unwrap()
}
}
#[test]
fn as_vec_no_final_null() {
let strtab = Strtab::new_preparsed(b"\0printf\0memmove\0busta", 0x0).unwrap();
let vec = strtab.to_vec().unwrap();
assert_eq!(vec.len(), 4);
assert_eq!(vec, vec!["", "printf", "memmove", "busta"]);
}
#[test]
fn as_vec_no_first_null_no_final_null() {
let strtab = Strtab::new_preparsed(b"printf\0memmove\0busta", 0x0).unwrap();
let vec = strtab.to_vec().unwrap();
assert_eq!(vec.len(), 3);
assert_eq!(vec, vec!["printf", "memmove", "busta"]);
}
#[test]
fn to_vec_final_null() {
let strtab = Strtab::new_preparsed(b"\0printf\0memmove\0busta\0", 0x0).unwrap();
let vec = strtab.to_vec().unwrap();
assert_eq!(vec.len(), 4);
assert_eq!(vec, vec!["", "printf", "memmove", "busta"]);
}
#[test]
fn to_vec_newline_delim() {
let strtab = Strtab::new_preparsed(b"\nprintf\nmemmove\nbusta\n", b'\n').unwrap();
let vec = strtab.to_vec().unwrap();
assert_eq!(vec.len(), 4);
assert_eq!(vec, vec!["", "printf", "memmove", "busta"]);
}
#[test]
fn parse_utf8() {
assert!(match Strtab::new_preparsed(&[0x80, 0x80], b'\n') {
Err(error::Error::Scroll(scroll::Error::BadInput {
size: 2,
msg: "invalid utf8",
})) => true,
_ => false,
});
assert!(
match Strtab::new_preparsed(&[0xC6, 0x92, 0x6F, 0x6F], b'\n') {
Ok(_) => true,
_ => false,
}
);
}
#[test]
fn get_at_utf8() {
let strtab = Strtab::new_preparsed("\nƒoo\nmemmove\n🅱️usta\n".as_bytes(), b'\n').unwrap();
assert_eq!(strtab.get_at(0), Some(""));
assert_eq!(strtab.get_at(5), Some(""));
assert_eq!(strtab.get_at(6), Some("memmove"));
assert_eq!(strtab.get_at(14), Some("\u{1f171}\u{fe0f}usta"));
assert_eq!(strtab.get_at(16), None);
assert_eq!(strtab.get_at(18), Some("\u{fe0f}usta"));
assert_eq!(strtab.get_at(21), Some("usta"));
assert_eq!(strtab.get_at(25), Some(""));
assert_eq!(strtab.get_at(26), None);
}