From daf229c1e9020570d69bf219f04aa2250788b23c Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Sat, 29 Nov 2025 15:03:29 +0000 Subject: [PATCH] Implement PE executable version parsing Using only the standard library (i.e. without pelite). This commit keeps the pelite dependency and uses it to check that the new parser gives the same result. The next commit will remove pelite. I've verified that both parsers give the same result for the following files that are referenced in the masterlists' version conditions: Starfield.exe sfse_1_15_222.dll SkyrimSE.exe skse64_loader.exe skseVR_loader.exe ENBHelperSE.dll d3d11.dll EngineFixes.dll QuickLootEE.dll FalloutNV.exe nvse_1_4.dll nvse_1_4ng.dll nvse_1_1.dll Fallout3.exe fose_1_7.dll fose_1_7ng.dll nvac.dll Fallout4.exe f4se_loader.exe f4sevr_loader.exe place.dll obse_1_2_416.dll TESV.exe skse_loader.exe The only referenced files that I couldn't test against were SkyrimVR.exe and Fallout4VR.exe, because I don't own those games. --- Cargo.toml | 2 +- src/function/version.rs | 44 ++- src/function/version/pe.rs | 701 +++++++++++++++++++++++++++++++++++++ 3 files changed, 739 insertions(+), 8 deletions(-) create mode 100644 src/function/version/pe.rs diff --git a/Cargo.toml b/Cargo.toml index 42e9836..a34cc40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ unsafe_code = "deny" [workspace.lints.clippy] pedantic = { level = "deny", priority = -1 } allow_attributes = "deny" -as_conversions = "forbid" +as_conversions = "deny" as_underscore = "forbid" assertions_on_result_states = "deny" big_endian_bytes = "forbid" diff --git a/src/function/version.rs b/src/function/version.rs index 8ff56a9..336451d 100644 --- a/src/function/version.rs +++ b/src/function/version.rs @@ -1,3 +1,5 @@ +mod pe; + use std::cmp::Ordering; use std::path::Path; @@ -5,6 +7,7 @@ use pelite::resources::version_info::VersionInfo; use pelite::resources::{FindError, Resources}; use crate::error::Error; +use pe::{read_file_version, read_pe_version, read_product_version}; #[derive(Clone, Debug)] enum ReleaseId { @@ -110,7 +113,9 @@ pub(super) struct Version { impl Version { pub(super) fn read_file_version(file_path: &Path) -> Result, Error> { - Self::read_version(file_path, |v| { + let version = read_pe_version(file_path, read_file_version)?; + + let pelite_version = Self::read_version(file_path, |v| { v.fixed().map(|f| { format!( "{}.{}.{}.{}", @@ -120,19 +125,25 @@ impl Version { f.dwFileVersion.Build ) }) - }) + }); + + compare_versions(file_path, version, pelite_version) } pub(super) fn read_product_version(file_path: &Path) -> Result, Error> { - Self::read_version(file_path, |v| { + let version = read_pe_version(file_path, read_product_version)?; + + let pelite_version = Self::read_version(file_path, |v| { v.translation() .first() .and_then(|language| v.value(*language, "ProductVersion")) - }) + }); + + compare_versions(file_path, version, pelite_version) } pub(super) fn is_readable(file_path: &Path) -> bool { - Self::read_version(file_path, |_| None).is_ok() + read_pe_version(file_path, |_| Ok(None)).is_ok() } fn read_version Option>( @@ -163,6 +174,25 @@ impl Version { } } +fn compare_versions( + file_path: &Path, + version: Option, + pelite_version: Result, Error>, +) -> Result, Error> { + match pelite_version { + Ok(pv) if pv == version => Ok(version), + Ok(pv) => Err(Error::PeParsingError( + file_path.to_path_buf(), + format!("Versions parsed by object and pelite don't match: {version:?} != {pv:?}") + .into(), + )), + Err(e) => Err(Error::PeParsingError( + file_path.to_path_buf(), + format!("pelite errored when parsing version but built-in parser did not: {e}").into(), + )), + } +} + fn get_pe_version_info(bytes: &[u8]) -> Result, FindError> { get_pe_resources(bytes)?.version_info() } @@ -442,7 +472,7 @@ mod tests { fn version_read_file_version_should_error_with_path_if_the_file_is_not_an_executable() { let error = Version::read_file_version(Path::new("Cargo.toml")).unwrap_err(); - assert_eq!("An error was encountered while reading the version fields of \"Cargo.toml\": unknown magic number", error.to_string()); + assert_eq!("An error was encountered while reading the version fields of \"Cargo.toml\": Unknown file magic", error.to_string()); } #[test] @@ -529,7 +559,7 @@ mod tests { fn version_read_product_version_should_error_with_path_if_the_file_is_not_an_executable() { let error = Version::read_product_version(Path::new("Cargo.toml")).unwrap_err(); - assert_eq!("An error was encountered while reading the version fields of \"Cargo.toml\": unknown magic number", error.to_string()); + assert_eq!("An error was encountered while reading the version fields of \"Cargo.toml\": Unknown file magic", error.to_string()); } #[test] diff --git a/src/function/version/pe.rs b/src/function/version/pe.rs new file mode 100644 index 0000000..03c9edf --- /dev/null +++ b/src/function/version/pe.rs @@ -0,0 +1,701 @@ +use std::{ + fs::File, + io::{BufReader, Read, Seek, SeekFrom}, + path::Path, +}; + +use crate::Error; + +use super::{ReleaseId, Version}; + +const KEY_OFFSET: usize = 6; + +struct StructHeaders { + length: usize, + value_length: usize, +} + +enum ReadResult { + Version(String), + NewOffset(usize), +} + +pub(super) fn read_pe_version( + file_path: &Path, + read_from_version_info: F, +) -> Result, Error> +where + F: Fn(&[u8]) -> Result, String>, +{ + let file = File::open(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?; + let mut reader = BufReader::new(file); + + let data = read_version_resource_data(&mut reader) + .map_err(|e| Error::PeParsingError(file_path.to_path_buf(), e.into()))?; + + if let Some(data) = data { + read_from_version_info(&data) + .map_err(|e| Error::PeParsingError(file_path.to_path_buf(), e.into())) + } else { + Ok(None) + } +} + +// +// +fn read_version_resource_data(reader: &mut T) -> std::io::Result>> { + const DOS_MAGIC: &[u8; 2] = b"MZ"; + const PE_MAGIC: &[u8; 4] = b"PE\0\0"; + const PE_HEADER_OFFSET_OFFSET: u64 = 0x3C; + + let mut dos_magic = [0u8; 2]; + reader.read_exact(&mut dos_magic)?; + + if &dos_magic != DOS_MAGIC { + return Err(std::io::Error::other("Unknown file magic")); + } + + reader.seek(std::io::SeekFrom::Start(PE_HEADER_OFFSET_OFFSET))?; + + let mut pe_header_offset = [0u8; 2]; + reader.read_exact(&mut pe_header_offset)?; + + let pe_header_offset = u64::from(u16::from_le_bytes(pe_header_offset)); + + reader.seek(std::io::SeekFrom::Start(pe_header_offset))?; + + let mut pe_magic = [0u8; 4]; + reader.read_exact(&mut pe_magic)?; + + if &pe_magic != PE_MAGIC { + return Err(std::io::Error::other("Unexpected PE header magic bytes")); + } + + let coff_header = CoffFileHeader::read(reader)?; + + let optional_header = + OptionalHeader::read(reader, u64::from(coff_header.optional_header_size))?; + + let Some(resource_table_data_directory) = optional_header.resource_table_data_directory() + else { + return Ok(None); + }; + + for _ in 0..coff_header.number_of_sections { + let entry = SectionTableEntry::read(reader)?; + + // The table entry offsets within the resource table are relative to + // the start of the table, so it's easier to read the data into a buffer + // and then seek around inside it. + if let Some(table_data) = resource_table_data_directory.read_data(reader, &entry)? { + return read_version_data(&entry, &table_data); + } + } + + Ok(None) +} + +fn read_version_data( + table_entry: &SectionTableEntry, + resource_table_data: &[u8], +) -> std::io::Result>> { + let mut cursor = std::io::Cursor::new(&resource_table_data); + + let Some(version_data_entry) = read_resource_tables(&mut cursor)? else { + return Ok(None); + }; + + // Unlike the table entry offsets, the version data's offset is given + // relative to the start of the loaded executable's virtual address. + let data_offset = version_data_entry.data_rva - table_entry.virtual_address; + + cursor.seek(SeekFrom::Start(u64::from(data_offset)))?; + + let mut version_data = vec![0; to_usize(version_data_entry.size)]; + cursor.read_exact(&mut version_data)?; + + Ok(Some(version_data)) +} + +/// This expects to be given a reader that's at the start of the resource table +/// data. +fn read_resource_tables( + reader: &mut T, +) -> std::io::Result> { + let root_table = ResourceDirectoryTable::read(reader)?; + + for root_entry in root_table.entries { + if root_entry.name_offset_or_id == ResourceDirectoryEntry::RT_VERSION + && root_entry.is_table() + { + reader.seek(SeekFrom::Start(u64::from(root_entry.offset())))?; + + let version_name_table = ResourceDirectoryTable::read(reader)?; + + for name_entry in version_name_table.entries { + if name_entry.is_table() { + reader.seek(SeekFrom::Start(u64::from(name_entry.offset())))?; + + let version_language_table = ResourceDirectoryTable::read(reader)?; + + for language_entry in version_language_table.entries { + if !language_entry.is_table() { + reader.seek(SeekFrom::Start(u64::from(language_entry.offset())))?; + + return ResourceDataEntry::read(reader).map(Some); + } + } + } + } + } + } + + Ok(None) +} + +#[derive(Debug)] +struct CoffFileHeader { + number_of_sections: u16, + optional_header_size: u16, +} + +impl CoffFileHeader { + fn read(reader: &mut T) -> std::io::Result { + let mut word = [0u8; 2]; + + // Skip machine. + reader.seek_relative(2)?; + + reader.read_exact(&mut word)?; + let number_of_sections = u16::from_le_bytes(word); + + // Skip time_date_stamp, symbol_table_offset and number_of_symbols. + reader.seek_relative(12)?; + + reader.read_exact(&mut word)?; + let optional_header_size = u16::from_le_bytes(word); + + // Optional header is required for executables. + if optional_header_size == 0 { + return Err(std::io::Error::other( + "COFF optional header is unexpectedly missing", + )); + } + + // Skip characteristics. + reader.seek_relative(2)?; + + Ok(Self { + number_of_sections, + optional_header_size, + }) + } +} + +#[derive(Debug)] +struct OptionalHeader { + image_data_directories: Vec, +} + +impl OptionalHeader { + const PE32_MAGIC: u16 = 0x10b; + const RESOURCE_TABLE_DATA_DIRECTORY_OFFSET: usize = 2; + + /// Ensure that reading the optional header is restricted to the declared + /// size of the header, since otherwise an invalid number_of_rva_and_sizes + /// field value could cause the reader to go past the declared end of the + /// header. + fn read(reader: &mut T, header_size: u64) -> std::io::Result { + let reader = &mut reader.take(header_size); + let mut word = [0u8; 2]; + + reader.read_exact(&mut word)?; + let magic = u16::from_le_bytes(word); + + // Skip many fields. + if magic == Self::PE32_MAGIC { + reader.seek_relative(90)?; + } else { + reader.seek_relative(106)?; + } + + let mut dword = [0u8; 4]; + + reader.read_exact(&mut dword)?; + let number_of_rva_and_sizes = u32::from_le_bytes(dword); + + let mut image_data_directories = Vec::with_capacity(to_usize(number_of_rva_and_sizes)); + for _ in 0..number_of_rva_and_sizes { + image_data_directories.push(ImageDataDirectory::read(reader)?); + } + + Ok(OptionalHeader { + image_data_directories, + }) + } + + fn resource_table_data_directory(&self) -> Option<&ImageDataDirectory> { + self.image_data_directories + .get(OptionalHeader::RESOURCE_TABLE_DATA_DIRECTORY_OFFSET) + } +} + +#[derive(Debug)] +struct ImageDataDirectory { + virtual_address: u32, + size: u32, +} + +impl ImageDataDirectory { + fn read(reader: &mut T) -> std::io::Result { + let mut dword = [0u8; 4]; + + reader.read_exact(&mut dword)?; + let virtual_address = u32::from_le_bytes(dword); + + reader.read_exact(&mut dword)?; + let size = u32::from_le_bytes(dword); + + Ok(Self { + virtual_address, + size, + }) + } + + fn read_data( + &self, + reader: &mut T, + entry: &SectionTableEntry, + ) -> std::io::Result>> { + if entry.contains(self) { + let table_offset = self.virtual_address - entry.virtual_address; + + reader.seek(std::io::SeekFrom::Start(u64::from( + entry.raw_data_offset + table_offset, + )))?; + + let mut data = vec![0; to_usize(self.size)]; + reader.read_exact(&mut data)?; + + Ok(Some(data)) + } else { + Ok(None) + } + } +} + +#[derive(Debug)] +struct SectionTableEntry { + virtual_size: u32, + virtual_address: u32, + raw_data_size: u32, + raw_data_offset: u32, +} + +impl SectionTableEntry { + fn read(reader: &mut T) -> std::io::Result { + let mut name = [0u8; 8]; + reader.read_exact(&mut name)?; + + let mut dword = [0u8; 4]; + + reader.read_exact(&mut dword)?; + let virtual_size = u32::from_le_bytes(dword); + + reader.read_exact(&mut dword)?; + let virtual_address = u32::from_le_bytes(dword); + + reader.read_exact(&mut dword)?; + let raw_data_size = u32::from_le_bytes(dword); + + reader.read_exact(&mut dword)?; + let raw_data_offset = u32::from_le_bytes(dword); + + // Skip relocations_offset, line_numbers_offset, relocations_count, + // line_numbers_count and characteristics. + reader.seek_relative(16)?; + + Ok(Self { + virtual_size, + virtual_address, + raw_data_size, + raw_data_offset, + }) + } + + fn contains(&self, image_data_directory: &ImageDataDirectory) -> bool { + let section_end = self.virtual_address + self.actual_size(); + let directory_end = image_data_directory.virtual_address + image_data_directory.size; + + image_data_directory.virtual_address >= self.virtual_address && directory_end <= section_end + } + + fn actual_size(&self) -> u32 { + std::cmp::min(self.raw_data_size, self.virtual_size) + } +} + +#[derive(Debug)] +struct ResourceDirectoryTable { + entries: Vec, +} + +impl ResourceDirectoryTable { + fn read(reader: &mut T) -> std::io::Result { + // Skip characteristics, time_date_stamp, major_version and minor_version. + reader.seek_relative(12)?; + + let mut word = [0u8; 2]; + + reader.read_exact(&mut word)?; + let name_entry_count = u16::from_le_bytes(word); + + reader.read_exact(&mut word)?; + let id_entry_count = u16::from_le_bytes(word); + + let mut entries = Vec::with_capacity(usize::from(name_entry_count + id_entry_count)); + for _ in 0..name_entry_count { + entries.push(ResourceDirectoryEntry::read(reader)?); + } + + for _ in 0..id_entry_count { + entries.push(ResourceDirectoryEntry::read(reader)?); + } + + Ok(Self { entries }) + } +} + +#[derive(Debug)] +struct ResourceDirectoryEntry { + name_offset_or_id: u32, + data_entry_or_subdirectory_offset: u32, +} + +impl ResourceDirectoryEntry { + const RT_VERSION: u32 = 16; + + fn read(reader: &mut T) -> std::io::Result { + let mut dword = [0u8; 4]; + + reader.read_exact(&mut dword)?; + let name_offset_or_id = u32::from_le_bytes(dword); + + reader.read_exact(&mut dword)?; + let data_entry_or_subdirectory_offset = u32::from_le_bytes(dword); + + Ok(Self { + name_offset_or_id, + data_entry_or_subdirectory_offset, + }) + } + + fn is_table(&self) -> bool { + (self.data_entry_or_subdirectory_offset & (1 << 31)) != 0 + } + + fn offset(&self) -> u32 { + if self.is_table() { + self.data_entry_or_subdirectory_offset ^ (1 << 31) + } else { + self.data_entry_or_subdirectory_offset + } + } +} + +#[derive(Debug)] +struct ResourceDataEntry { + data_rva: u32, + size: u32, +} + +impl ResourceDataEntry { + fn read(reader: &mut T) -> std::io::Result { + let mut dword = [0u8; 4]; + + reader.read_exact(&mut dword)?; + let data_rva = u32::from_le_bytes(dword); + + reader.read_exact(&mut dword)?; + let size = u32::from_le_bytes(dword); + + // Skip codepage and reserved. + reader.seek_relative(8)?; + + Ok(Self { data_rva, size }) + } +} + +#[expect( + clippy::as_conversions, + reason = "A compile-time assertion ensures that this conversion will be lossless on all relevant target platforms" +)] +const fn to_usize(value: u32) -> usize { + // Error at compile time if this conversion isn't lossless. + const _: () = assert!(u32::BITS <= usize::BITS, "cannot fit a u32 into a usize!"); + value as usize +} + +fn subslice(bytes: &[u8], offset: usize, size: usize) -> Result<&[u8], String> { + bytes.get(offset..offset + size).ok_or_else(|| { + format!("Invalid subslice of size {size} at offset {offset} of bytes {bytes:X?}") + }) +} + +// +pub(super) fn read_file_version(data: &[u8]) -> Result, String> { + let StructHeaders { value_length, .. } = read_vs_version_info_headers(data)?; + + if value_length == 0 { + Ok(None) + } else { + let fixed_file_info = subslice(data, 40, value_length)?; + Some(read_vs_fixed_file_info(fixed_file_info)).transpose() + } +} + +fn read_vs_version_info_headers(data: &[u8]) -> Result { + let headers = read_struct_headers(data)?; + + if headers.length != data.len() { + return Err(format!( + "Unexpected length of VS_VERSIONINFO struct, got {} but buffer length is {}", + headers.length, + data.len() + )); + } + + if !has_subslice_at( + data, + KEY_OFFSET, + b"V\0S\0_\0V\0E\0R\0S\0I\0O\0N\0_\0I\0N\0F\0O\0\0\0", + ) { + return Err(format!( + "The szKey field's value is not valid for a VS_VERSIONINFO struct: {data:X?}" + )); + } + + Ok(headers) +} + +fn read_struct_headers(data: &[u8]) -> Result { + let [l0, l1, vl0, vl1, ..] = data else { + return Err(format!( + "The buffer was too small to hold a struct: {data:X?}" + )); + }; + + let length = usize::from(u16::from_le_bytes([*l0, *l1])); + + let value_length = usize::from(u16::from_le_bytes([*vl0, *vl1])); + + Ok(StructHeaders { + length, + value_length, + }) +} + +fn has_subslice_at(haystack: &[u8], offset: usize, needle: &[u8]) -> bool { + haystack + .get(offset..offset + needle.len()) + .is_some_and(|s| s == needle) +} + +// +fn read_vs_fixed_file_info(data: &[u8]) -> Result { + const VS_FIXEDFILEINFO_SIZE: usize = 0x34; + const VS_FIXEDFILEINFO_SIGNATURE: [u8; 4] = [0xBD, 0x04, 0xEF, 0xFE]; + const FILE_VERSION_OFFSET: usize = 8; + const FILE_VERSION_LENGTH: usize = 8; + + if data.len() != VS_FIXEDFILEINFO_SIZE { + return Err(format!( + "Unexpected length of VS_VERSIONINFO value, got {} but expected {}", + data.len(), + VS_FIXEDFILEINFO_SIZE + )); + } + + if !has_subslice_at(data, 0, &VS_FIXEDFILEINFO_SIGNATURE) { + return Err(format!( + "Unexpected first four bytes of VS_VERSIONINFO struct {data:X?}" + )); + } + + let Some( + [file_minor_0, file_minor_1, file_major_0, file_major_1, file_build_0, file_build_1, file_patch_0, file_patch_1], + ) = data.get(FILE_VERSION_OFFSET..FILE_VERSION_OFFSET + FILE_VERSION_LENGTH) + else { + return Err(format!( + "The buffer was too small to hold a VS_FIXEDFILEINFO struct: {data:X?}" + )); + }; + + let file_minor = u16::from_le_bytes([*file_minor_0, *file_minor_1]); + let file_major = u16::from_le_bytes([*file_major_0, *file_major_1]); + let file_build = u16::from_le_bytes([*file_build_0, *file_build_1]); + let file_patch = u16::from_le_bytes([*file_patch_0, *file_patch_1]); + + Ok(Version { + release_ids: vec![ + ReleaseId::Numeric(u32::from(file_major)), + ReleaseId::Numeric(u32::from(file_minor)), + ReleaseId::Numeric(u32::from(file_patch)), + ReleaseId::Numeric(u32::from(file_build)), + ], + pre_release_ids: Vec::new(), + }) +} + +// +pub(super) fn read_product_version(data: &[u8]) -> Result, String> { + const CHILDREN_BASE_OFFSET: usize = 40; + + let StructHeaders { + length, + value_length, + } = read_vs_version_info_headers(data)?; + + let mut children = subslice( + data, + CHILDREN_BASE_OFFSET + value_length, + length - (CHILDREN_BASE_OFFSET + value_length), + )?; + + while !children.is_empty() { + let next_offset = match read_next_child(children)? { + ReadResult::NewOffset(offset) => offset, + ReadResult::Version(version) => return Ok(Some(Version::from(version))), + }; + + children = offset(children, next_offset)?; + } + + Ok(None) +} + +fn read_next_child(children: &[u8]) -> Result { + const STRING_FILE_INFO_KEY: &[u8; 30] = b"S\0t\0r\0i\0n\0g\0F\0i\0l\0e\0I\0n\0f\0o\0\0\0"; + + let child_length = read_struct_size(children)?; + + if has_subslice_at(children, KEY_OFFSET, STRING_FILE_INFO_KEY) { + // + const STRING_TABLES_OFFSET: usize = KEY_OFFSET + STRING_FILE_INFO_KEY.len(); + + if child_length < STRING_TABLES_OFFSET { + return Err(format!( + "The StringFileInfo struct's header is too small: {child_length}" + )); + } + + let mut string_tables = subslice( + children, + STRING_TABLES_OFFSET, + child_length - STRING_TABLES_OFFSET, + )?; + + while !string_tables.is_empty() { + let next_offset = match read_next_string_table(string_tables)? { + ReadResult::NewOffset(offset) => offset, + ReadResult::Version(version) => return Ok(ReadResult::Version(version)), + }; + + string_tables = offset(children, next_offset)?; + } + } + + Ok(ReadResult::NewOffset(new_aligned_offset(child_length))) +} + +fn read_struct_size(buffer: &[u8]) -> Result { + buffer + .first_chunk::<2>() + .map(|c| usize::from(u16::from_le_bytes(*c))) + .ok_or_else( + || format!("The buffer was too small to hold a struct size field: {buffer:X?}",), + ) +} + +// +fn read_next_string_table(string_tables: &[u8]) -> Result { + const STRINGS_OFFSET: usize = 24; + + let string_table_length = read_struct_size(string_tables)?; + + if string_table_length < STRINGS_OFFSET { + return Err(format!( + "The StringTable struct's header is too small: {string_table_length}" + )); + } + + let mut strings = subslice( + string_tables, + STRINGS_OFFSET, + string_table_length - STRINGS_OFFSET, + )?; + + while !strings.is_empty() { + let next_offset = match read_next_string(strings)? { + ReadResult::NewOffset(offset) => offset, + ReadResult::Version(version) => return Ok(ReadResult::Version(version)), + }; + + strings = offset(strings, next_offset)?; + } + + Ok(ReadResult::NewOffset(new_aligned_offset( + string_table_length, + ))) +} + +// +fn read_next_string(strings: &[u8]) -> Result { + const PRODUCT_VERSION_KEY: &[u8; 30] = b"P\0r\0o\0d\0u\0c\0t\0V\0e\0r\0s\0i\0o\0n\0\0\0"; + const VALUE_OFFSET: usize = KEY_OFFSET + PRODUCT_VERSION_KEY.len(); + + let Ok(headers) = read_struct_headers(strings) else { + return Err(format!( + "The buffer was too small to hold a String struct: {strings:X?}" + )); + }; + + if has_subslice_at(strings, KEY_OFFSET, PRODUCT_VERSION_KEY) { + let string_bytes = subslice(strings, VALUE_OFFSET, headers.value_length * 2)?; + let utf8_string = read_utf16_string(string_bytes).map_err(|e| e.to_string())?; + return Ok(ReadResult::Version(utf8_string)); + } + + Ok(ReadResult::NewOffset(new_aligned_offset(headers.length))) +} + +fn offset(bytes: &[u8], offset: usize) -> Result<&[u8], String> { + bytes + .get(offset..) + .ok_or_else(|| format!("Failed to get subslice at offset {offset} of {bytes:X?}")) +} + +fn new_aligned_offset(length_read: usize) -> usize { + if length_read.is_multiple_of(4) { + length_read + } else { + length_read + 2 + } +} + +fn read_utf16_string(bytes: &[u8]) -> Result { + // This could be made more efficient by checking alignment and transmuting + // the slice if aligned, but that involves unsafe, and there isn't a + // significant performance difference. + let mut u16_vec: Vec = bytes + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) + .collect(); + + // We don't want to keep the trailing null u16. + u16_vec.pop(); + + String::from_utf16(&u16_vec) +}