Add a simple StringTable wrapper around string table section bytes

This interprets the string table section's data bytes as &str references with
lifetime checking bound to the underlying section data lifetime, so the
underyling data bytes for the string contents aren't copied or re-allocated
when getting a string from the table.

It does do two read passes over the string contents, first to find the
terminating NUL byte and second to do Utf8 validation checking.

I opted to do the Utf8 checking because my goal for this crate is to have zero
unsafe code in it, and the unchecked from_utf8 is unsafe. I have not yet
thought through what opinion I want this crate to have w.r.t. reading malformed
data in general but so far I've been making it return errors as it encounters
it.
This commit is contained in:
Christopher Cole 2022-10-10 13:55:46 -07:00
parent a7ce73ee98
commit 3455f4dd1e
No known key found for this signature in database
GPG Key ID: 0AC856975983E9DB
2 changed files with 84 additions and 0 deletions

View File

@ -13,6 +13,7 @@ pub mod parse;
use crate::parse::{Parse, Reader, read_u16, read_u32, read_u64};
mod utils;
mod string_table;
pub struct File {
pub ehdr: file::FileHeader,
@ -58,6 +59,12 @@ impl std::convert::From<std::io::Error> for ParseError {
}
}
impl std::convert::From<std::str::Utf8Error> for ParseError {
fn from(e: std::str::Utf8Error) -> Self {
ParseError(e.to_string())
}
}
impl std::convert::From<std::string::FromUtf8Error> for ParseError {
fn from(e: std::string::FromUtf8Error) -> Self {
ParseError(e.to_string())

77
src/string_table.rs Normal file
View File

@ -0,0 +1,77 @@
use crate::ParseError;
#[derive(Debug, Clone, Copy)]
pub struct StringTable<'data> {
#[allow(dead_code)]
data: Option<&'data [u8]>,
}
impl<'data> StringTable<'data> {
#[allow(dead_code)]
pub fn new(data: &'data [u8]) -> Self {
StringTable { data: Some(data) }
}
#[allow(dead_code)]
pub fn get(&self, offset: usize) -> Result<&'data str, ParseError> {
if self.data.is_none() {
return Err(ParseError(format!(
"Invalid offset into empty string table: {offset}"
)));
}
let start = self.data.unwrap().split_at(offset).1;
let end = start
.iter()
.position(|&b| b == 0u8)
.ok_or(ParseError(format!(
"Invalid string table contents. Could not find terminating NUL byte."
)))?;
let substr = start.split_at(end).0;
let string = std::str::from_utf8(substr)?;
Ok(string)
}
}
impl<'data> Default for StringTable<'data> {
fn default() -> Self {
StringTable { data: None }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_table_errors() {
let st = StringTable::default();
assert!(st.get(0).is_err());
assert!(st.get(1).is_err());
}
/// Note: ELF string tables are defined to always start with a NUL and use
/// index 0 to give an empty string, so getting a string starting at a NUL
/// should properly give an empty string.
#[test]
fn test_get_index_0_gives_empty_string() {
let data = [0u8, 42u8, 0u8];
let st = StringTable::new(&data);
assert_eq!(st.get(0).unwrap(), "");
}
#[test]
fn test_get_string_works() {
let data = [0u8, 0x45, 0x4C, 0x46, 0u8];
let st = StringTable::new(&data);
assert_eq!(st.get(1).unwrap(), "ELF");
}
#[test]
fn test_get_with_malformed_table_no_trailing_nul() {
let data = [0u8, 0x45, 0x4C, 0x46];
let st = StringTable::new(&data);
assert!(st.get(1).is_err());
}
}