use std::{
collections::{HashMap, HashSet},
sync::{Arc, OnceLock},
};
use pascalscript::Container;
use crate::{
analysis::{exec::ExecIter, registryop::RegistryOpIter, shortcut::ShortcutIter},
crypto::{
kdflegacy::{LegacyHashFamily, LegacyStoredHash, legacy_hash_family},
xchacha20::{SpecialContext, special_nonce},
},
decompress::block::{BlockCompression, decompress_block, decompress_block_with_decryption},
error::Error,
extract::{chunk::decompress_chunk, file::FileReader, slice::SliceReader},
header::{Architecture, HeaderAnsi, HeaderString, SetupHeader},
overlay::{
OffsetTable,
offsettable::SetupLdrFamily,
pe::{self, OffsetTableLocation},
},
records::{
component::ComponentEntry,
dataentry::DataEntry,
delete::DeleteEntry,
directory::DirectoryEntry,
file::{FileEntry, FileEntryType},
icon::IconEntry,
ini::IniEntry,
isssigkey::ISSigKeyEntry,
language::LanguageEntry,
message::MessageEntry,
permission::PermissionEntry,
registry::RegistryEntry,
run::RunEntry,
task::TaskEntry,
type_::TypeEntry,
},
util::read::Reader,
version::{Variant, Version, read_marker},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Compression {
Stored,
Zlib,
Lzma1,
Unknown,
}
stable_name_enum!(Compression, {
Self::Stored => "stored",
Self::Zlib => "zlib",
Self::Lzma1 => "lzma1",
Self::Unknown => "unknown",
});
impl Compression {
fn from_block(c: BlockCompression) -> Self {
match c {
BlockCompression::Stored => Self::Stored,
BlockCompression::Zlib => Self::Zlib,
BlockCompression::Lzma1 => Self::Lzma1,
}
}
}
#[derive(Clone, Debug)]
pub struct EncryptionInfo {
pub mode: EncryptionMode,
pub salt: [u8; 16],
pub kdf_iterations: u32,
pub base_nonce: [u8; 24],
pub password_test: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum EncryptionMode {
None,
Files,
Full,
}
stable_name_enum!(EncryptionMode, {
Self::None => "none",
Self::Files => "files",
Self::Full => "full",
});
#[derive(Debug)]
pub struct InnoInstaller<'a> {
input: &'a [u8],
pe_location: OffsetTableLocation,
offset_table: OffsetTable,
version: Version,
variant: Variant,
encryption: Option<EncryptionInfo>,
compression: Compression,
decompressed_setup0: Box<[u8]>,
decompressed_data_block: Box<[u8]>,
header: Option<SetupHeader>,
records: ParsedRecords,
chunk_cache: Box<[OnceLock<Arc<[u8]>>]>,
file_loc_to_chunk: Box<[u32]>,
encryption_key: Option<[u8; 32]>,
legacy_password: Option<String>,
password_used: Option<String>,
}
#[derive(Clone, Debug, Default)]
struct ParsedRecords {
languages: Vec<LanguageEntry>,
messages: Vec<MessageEntry>,
permissions: Vec<PermissionEntry>,
types: Vec<TypeEntry>,
components: Vec<ComponentEntry>,
tasks: Vec<TaskEntry>,
directories: Vec<DirectoryEntry>,
iss_sig_keys: Vec<ISSigKeyEntry>,
files: Vec<FileEntry>,
icons: Vec<IconEntry>,
ini_entries: Vec<IniEntry>,
registry: Vec<RegistryEntry>,
install_deletes: Vec<DeleteEntry>,
uninstall_deletes: Vec<DeleteEntry>,
run: Vec<RunEntry>,
uninstall_run: Vec<RunEntry>,
file_locations: Vec<DataEntry>,
}
impl<'a> InnoInstaller<'a> {
pub fn from_bytes(data: &'a [u8]) -> Result<Self, Error> {
Self::parse(data, None)
}
pub fn from_bytes_with_passwords(data: &'a [u8], passwords: &[&str]) -> Result<Self, Error> {
Self::parse(data, Some(passwords))
}
fn parse(data: &'a [u8], passwords: Option<&[&str]>) -> Result<Self, Error> {
let pe_location = pe::locate(data)?;
let offset_table = OffsetTable::parse(data, pe_location.start, pe_location.len)?;
let setup0_start = usize_from_u64(offset_table.offset_setup0, "Offset0")?;
let setup0 = data.get(setup0_start..).ok_or(Error::Truncated {
what: "setup-0 region",
})?;
let (version, variant) = read_marker(setup0)?;
let mut after_marker = Reader::at(setup0, 64)?;
let encryption = if version.at_least(6, 5, 0) {
parse_encryption_header(&mut after_marker)?
} else {
None
};
let block_start_in_setup0 = after_marker.pos();
let block_is_encrypted = matches!(
encryption.as_ref().map(|e| e.mode),
Some(EncryptionMode::Full),
);
let (mut encryption_key, mut password_used): (Option<[u8; 32]>, Option<String>) =
(None, None);
if let Some(candidates) = passwords
&& let Some(info) = encryption.as_ref()
&& matches!(info.mode, EncryptionMode::Files | EncryptionMode::Full)
{
if candidates.is_empty() {
return Err(Error::PasswordRequired);
}
let (key, used) = try_passwords(info, candidates, &version)?;
encryption_key = key;
password_used = used;
}
let (decompressed_setup0, decompressed_data_block, compression) =
if !version.at_least(4, 0, 9) {
(
Box::<[u8]>::default(),
Box::<[u8]>::default(),
Compression::Unknown,
)
} else if block_is_encrypted {
if let (Some(info), Some(key)) = (encryption.as_ref(), encryption_key.as_ref()) {
decompress_blocks_eufull(
setup0,
block_start_in_setup0,
&version,
&info.base_nonce,
key,
)?
} else {
(
Box::<[u8]>::default(),
Box::<[u8]>::default(),
Compression::Unknown,
)
}
} else {
let block1 = decompress_block(setup0, block_start_in_setup0, &version)?;
let comp = Compression::from_block(block1.compression);
let block2_start =
block_start_in_setup0
.checked_add(block1.consumed)
.ok_or(Error::Overflow {
what: "second block start",
})?;
let block2 = decompress_block(setup0, block2_start, &version)?;
(block1.bytes, block2.bytes, comp)
};
let header = if decompressed_setup0.is_empty() {
None
} else {
Some(SetupHeader::parse(&decompressed_setup0, &version)?)
};
let records = match header.as_ref() {
Some(h) => parse_records(&decompressed_setup0, &decompressed_data_block, h, &version)?,
None => ParsedRecords::default(),
};
let (chunk_cache, file_loc_to_chunk) = build_chunk_index(&records.file_locations);
let mut encryption = encryption;
if encryption.is_none()
&& let Some(h) = header.as_ref()
&& version.at_least(6, 4, 0)
&& !version.at_least(6, 5, 0)
&& h.has_option(crate::HeaderOption::Password)
{
let tail = h.tail();
if let (Some(test), Some(salt), Some(iters), Some(nonce)) = (
tail.password_test,
tail.encryption_kdf_salt,
tail.encryption_kdf_iterations,
tail.encryption_base_nonce,
) {
encryption = Some(EncryptionInfo {
mode: if h.has_option(crate::HeaderOption::EncryptionUsed) {
EncryptionMode::Files
} else {
EncryptionMode::None
},
salt,
kdf_iterations: iters,
base_nonce: nonce,
password_test: test,
});
}
}
if let Some(candidates) = passwords
&& encryption_key.is_none()
&& let Some(info) = encryption.as_ref()
{
if candidates.is_empty() {
return Err(Error::PasswordRequired);
}
let (key, used) = try_passwords(info, candidates, &version)?;
encryption_key = key;
password_used = used;
}
let mut legacy_password: Option<String> = None;
if let Some(candidates) = passwords
&& encryption.is_none()
&& let Some(stored) = header
.as_ref()
.filter(|h| h.has_option(crate::HeaderOption::Password))
.and_then(|h| legacy_stored_hash(h.tail(), &version))
{
if candidates.is_empty() {
return Err(Error::PasswordRequired);
}
let used = try_passwords_legacy(&stored, candidates, &version)?;
legacy_password = Some(used.clone());
password_used = Some(used);
}
Ok(Self {
input: data,
pe_location,
offset_table,
version,
variant,
encryption,
compression,
decompressed_setup0,
decompressed_data_block,
header,
records,
chunk_cache,
file_loc_to_chunk,
encryption_key,
legacy_password,
password_used,
})
}
#[must_use]
pub fn decompressed_setup0(&self) -> &[u8] {
&self.decompressed_setup0
}
#[must_use]
pub fn data_block(&self) -> &[u8] {
&self.decompressed_data_block
}
#[must_use]
pub fn header(&self) -> Option<&SetupHeader> {
self.header.as_ref()
}
#[must_use]
pub fn version(&self) -> &Version {
&self.version
}
#[must_use]
pub fn variant(&self) -> Variant {
self.variant
}
#[must_use]
pub fn offset_table(&self) -> &OffsetTable {
&self.offset_table
}
#[must_use]
pub fn setup_ldr_family(&self) -> SetupLdrFamily {
self.offset_table.source.family
}
#[must_use]
pub fn pe_locator_mode(&self) -> pe::LocatorMode {
self.pe_location.mode
}
#[must_use]
pub fn encryption(&self) -> Option<&EncryptionInfo> {
self.encryption.as_ref()
}
#[must_use]
pub fn compression(&self) -> Compression {
self.compression
}
#[must_use]
pub fn architecture(&self) -> Option<HashSet<Architecture>> {
let header = self.header.as_ref()?;
if let Some(set) = header.tail().architectures_allowed.clone() {
return Some(set);
}
let raw = header.string(HeaderString::ArchitecturesAllowed)?;
Some(parse_architecture_expression(raw))
}
#[must_use]
pub fn input(&self) -> &'a [u8] {
self.input
}
#[must_use]
pub fn license_text(&self) -> Option<&[u8]> {
self.header
.as_ref()
.and_then(|h| h.ansi(HeaderAnsi::LicenseText))
.filter(|b| !b.is_empty())
}
#[must_use]
pub fn info_before(&self) -> Option<&[u8]> {
self.header
.as_ref()
.and_then(|h| h.ansi(HeaderAnsi::InfoBeforeText))
.filter(|b| !b.is_empty())
}
#[must_use]
pub fn info_after(&self) -> Option<&[u8]> {
self.header
.as_ref()
.and_then(|h| h.ansi(HeaderAnsi::InfoAfterText))
.filter(|b| !b.is_empty())
}
#[must_use]
pub fn compiled_code_bytes(&self) -> Option<&[u8]> {
self.header
.as_ref()
.and_then(|h| h.ansi(HeaderAnsi::CompiledCodeText))
.filter(|b| !b.is_empty())
}
#[must_use]
pub fn compiledcode(&self) -> Option<Result<Container<'_>, Error>> {
self.compiled_code_bytes()
.map(|bytes| Container::parse(bytes).map_err(Error::from))
}
#[must_use]
pub fn inno_api_description(&self, name: &str) -> Option<&'static str> {
crate::analysis::compiledcode::inno_api_description(name)
}
#[must_use]
pub fn languages(&self) -> &[LanguageEntry] {
&self.records.languages
}
#[must_use]
pub fn messages(&self) -> &[MessageEntry] {
&self.records.messages
}
#[must_use]
pub fn permissions(&self) -> &[PermissionEntry] {
&self.records.permissions
}
#[must_use]
pub fn types(&self) -> &[TypeEntry] {
&self.records.types
}
#[must_use]
pub fn components(&self) -> &[ComponentEntry] {
&self.records.components
}
#[must_use]
pub fn tasks(&self) -> &[TaskEntry] {
&self.records.tasks
}
#[must_use]
pub fn directories(&self) -> &[DirectoryEntry] {
&self.records.directories
}
#[must_use]
pub fn iss_sig_keys(&self) -> &[ISSigKeyEntry] {
&self.records.iss_sig_keys
}
#[must_use]
pub fn files(&self) -> &[FileEntry] {
&self.records.files
}
#[must_use]
pub fn icons(&self) -> &[IconEntry] {
&self.records.icons
}
#[must_use]
pub fn ini_entries(&self) -> &[IniEntry] {
&self.records.ini_entries
}
#[must_use]
pub fn registry_entries(&self) -> &[RegistryEntry] {
&self.records.registry
}
#[must_use]
pub fn install_deletes(&self) -> &[DeleteEntry] {
&self.records.install_deletes
}
#[must_use]
pub fn uninstall_deletes(&self) -> &[DeleteEntry] {
&self.records.uninstall_deletes
}
#[must_use]
pub fn run_entries(&self) -> &[RunEntry] {
&self.records.run
}
#[must_use]
pub fn uninstall_runs(&self) -> &[RunEntry] {
&self.records.uninstall_run
}
#[must_use]
pub fn file_locations(&self) -> &[DataEntry] {
&self.records.file_locations
}
#[must_use]
pub fn file_location_for(&self, file: &FileEntry) -> Option<&DataEntry> {
usize::try_from(file.location_index)
.ok()
.and_then(|idx| self.records.file_locations.get(idx))
}
#[must_use]
pub fn exec_commands(&self) -> ExecIter<'_> {
ExecIter::new(self.run_entries(), self.uninstall_runs())
}
#[must_use]
pub fn registry_ops(&self) -> RegistryOpIter<'_> {
RegistryOpIter::new(self.registry_entries())
}
#[must_use]
pub fn shortcuts(&self) -> ShortcutIter<'_> {
ShortcutIter::new(self.icons(), self.files())
}
#[must_use]
pub fn is_encrypted(&self) -> bool {
match self.encryption.as_ref().map(|e| e.mode) {
Some(EncryptionMode::Files | EncryptionMode::Full) => true,
_ => self
.header
.as_ref()
.is_some_and(|h| h.has_option(crate::HeaderOption::Password)),
}
}
#[must_use]
pub fn password_used(&self) -> Option<&str> {
self.password_used.as_deref()
}
pub fn extract(&self, file: &FileEntry) -> Result<FileReader<'_>, Error> {
if file.location_index == u32::MAX {
return Err(Error::NoLocation);
}
self.extract_by_location(file.location_index)
}
pub fn extract_by_location(&self, location_index: u32) -> Result<FileReader<'_>, Error> {
let data = self
.records
.file_locations
.get(location_index as usize)
.ok_or(Error::Truncated {
what: "file_locations index",
})?;
let chunk_bytes = self.chunk_bytes_for(location_index)?;
FileReader::new(chunk_bytes, data, &self.version)
}
pub fn extract_to_vec(&self, file: &FileEntry) -> Result<Vec<u8>, Error> {
let mut reader = self.extract(file)?;
let mut out = Vec::with_capacity(reader.len());
std::io::Read::read_to_end(&mut reader, &mut out).map_err(|e| Error::Decompress {
stream: "extract_to_vec",
source: e,
})?;
Ok(out)
}
pub fn extract_uninstaller(&self) -> Result<Vec<u8>, Error> {
if !self
.files()
.iter()
.any(|f| f.file_type == Some(FileEntryType::UninstExe))
{
return Err(Error::NoLocation);
}
const MODE_OFFSET: usize = 0x30;
const MODE_UNINSTALLER: u32 = 0x6E55_6E49;
let mut bytes = self.input.to_vec();
let end = MODE_OFFSET.saturating_add(4);
let slot = bytes.get_mut(MODE_OFFSET..end).ok_or(Error::Truncated {
what: "SetupExeMode slot at offset 0x30",
})?;
slot.copy_from_slice(&MODE_UNINSTALLER.to_le_bytes());
Ok(bytes)
}
pub fn extract_files(&self) -> impl Iterator<Item = Result<(&FileEntry, Vec<u8>), Error>> + '_ {
self.files()
.iter()
.filter(|f| f.location_index != u32::MAX)
.map(move |f| self.extract_to_vec(f).map(|bytes| (f, bytes)))
}
fn encryption_context(&self) -> Option<crate::extract::chunk::EncryptionContext<'_>> {
if let (Some(key), Some(info)) = (self.encryption_key.as_ref(), self.encryption.as_ref()) {
return Some(crate::extract::chunk::EncryptionContext::Modern {
key,
base_nonce: &info.base_nonce,
mode: info.mode,
});
}
if let Some(password) = self.legacy_password.as_deref() {
return Some(crate::extract::chunk::EncryptionContext::Legacy {
password,
use_sha1: self.version.at_least(5, 3, 9),
unicode: crate::util::encoding::is_unicode_for_version(&self.version),
});
}
None
}
fn chunk_bytes_for(&self, location_index: u32) -> Result<&[u8], Error> {
let chunk_id =
*self
.file_loc_to_chunk
.get(location_index as usize)
.ok_or(Error::Truncated {
what: "file_loc_to_chunk lookup",
})?;
let slot = self
.chunk_cache
.get(chunk_id as usize)
.ok_or(Error::Truncated {
what: "chunk_cache lookup",
})?;
if let Some(arc) = slot.get() {
return Ok(arc.as_ref());
}
let data = self
.records
.file_locations
.get(location_index as usize)
.ok_or(Error::Truncated {
what: "file_locations index",
})?;
let compression = self
.header
.as_ref()
.and_then(|h| h.tail().compress_method)
.ok_or(Error::Truncated {
what: "header compression method",
})?;
let slice = SliceReader::embedded(self.input, self.offset_table.offset_setup1)?;
let bytes = decompress_chunk(
&slice,
data,
compression,
self.encryption_context().as_ref(),
)?;
let bytes = match slot.set(bytes) {
Ok(()) => slot.get().ok_or(Error::Truncated {
what: "OnceLock raced",
})?,
Err(_) => slot.get().ok_or(Error::Truncated {
what: "OnceLock raced",
})?,
};
Ok(bytes.as_ref())
}
}
fn try_passwords(
info: &EncryptionInfo,
passwords: &[&str],
version: &Version,
) -> Result<(Option<[u8; 32]>, Option<String>), Error> {
let derive: fn(&str, &[u8; 16], u32) -> [u8; 32] =
if (version.a, version.b, version.c, version.d) == (7, 0, 0, 1) {
crate::crypto::pbkdf2::derive_key_buggy_700_preview3
} else {
crate::crypto::pbkdf2::derive_key
};
for &password in passwords {
let key = derive(password, &info.salt, info.kdf_iterations);
let actual = crate::crypto::xchacha20::password_test_verifier(&key, &info.base_nonce);
if actual == info.password_test {
return Ok((Some(key), Some(password.to_owned())));
}
}
Err(Error::WrongPassword)
}
type DecryptedBlocks = (Box<[u8]>, Box<[u8]>, Compression);
fn decompress_blocks_eufull(
setup0: &[u8],
block_start: usize,
version: &Version,
base_nonce: &[u8; 24],
key: &[u8; 32],
) -> Result<DecryptedBlocks, Error> {
let nonce1 = special_nonce(base_nonce, SpecialContext::CompressedBlocks1);
let block1 = decompress_block_with_decryption(setup0, block_start, version, key, &nonce1)?;
let comp = Compression::from_block(block1.compression);
let block2_start = block_start
.checked_add(block1.consumed)
.ok_or(Error::Overflow {
what: "second block start",
})?;
let nonce2 = special_nonce(base_nonce, SpecialContext::CompressedBlocks2);
let block2 = decompress_block_with_decryption(setup0, block2_start, version, key, &nonce2)?;
Ok((block1.bytes, block2.bytes, comp))
}
fn parse_architecture_expression(s: &str) -> HashSet<Architecture> {
let lower = s.to_ascii_lowercase();
let mut set = HashSet::new();
let atoms: &[(&str, Architecture)] = &[
("x86compatible", Architecture::X86),
("x86os", Architecture::X86),
("x86", Architecture::X86),
("x64compatible", Architecture::Amd64),
("x64os", Architecture::Amd64),
("x64", Architecture::Amd64),
("arm32compatible", Architecture::Arm32),
("arm64", Architecture::Arm64),
("ia64", Architecture::IA64),
];
for (atom, arch) in atoms {
if lower.contains(atom) {
set.insert(*arch);
}
}
set
}
fn legacy_stored_hash(
tail: &crate::header::HeaderTail,
version: &Version,
) -> Option<LegacyStoredHash> {
match legacy_hash_family(version) {
LegacyHashFamily::Crc32 => tail.legacy_password_crc32.map(LegacyStoredHash::Crc32),
LegacyHashFamily::Md5Bare => tail.legacy_password_md5.map(LegacyStoredHash::Md5Bare),
LegacyHashFamily::Md5SaltedWithPrefix => {
match (tail.legacy_password_md5, tail.legacy_password_salt) {
(Some(hash), Some(salt)) => Some(LegacyStoredHash::Md5Salted { hash, salt }),
_ => None,
}
}
LegacyHashFamily::Sha1SaltedWithPrefix => {
match (tail.legacy_password_sha1, tail.legacy_password_salt) {
(Some(hash), Some(salt)) => Some(LegacyStoredHash::Sha1Salted { hash, salt }),
_ => None,
}
}
}
}
fn try_passwords_legacy(
stored: &crate::crypto::kdflegacy::LegacyStoredHash,
passwords: &[&str],
version: &Version,
) -> Result<String, Error> {
let unicode = crate::util::encoding::is_unicode_for_version(version);
for &candidate in passwords {
if crate::crypto::kdflegacy::verify_password_legacy(candidate, stored, unicode) {
return Ok(candidate.to_owned());
}
}
Err(Error::WrongPassword)
}
type ChunkIndex = (Box<[OnceLock<Arc<[u8]>>]>, Box<[u32]>);
fn build_chunk_index(file_locations: &[DataEntry]) -> ChunkIndex {
let mut keys: HashMap<(u32, u32), u32> = HashMap::new();
let mut next_id: u32 = 0;
let mut mapping: Vec<u32> = Vec::with_capacity(file_locations.len());
for d in file_locations {
let key = (d.first_slice, d.start_offset);
let id = *keys.entry(key).or_insert_with(|| {
let id = next_id;
next_id = next_id.saturating_add(1);
id
});
mapping.push(id);
}
let cache_len = next_id as usize;
let mut cache: Vec<OnceLock<Arc<[u8]>>> = Vec::with_capacity(cache_len);
for _ in 0..cache_len {
cache.push(OnceLock::new());
}
(cache.into_boxed_slice(), mapping.into_boxed_slice())
}
fn parse_records(
setup0: &[u8],
data_block: &[u8],
header: &SetupHeader,
version: &Version,
) -> Result<ParsedRecords, Error> {
let mut reader = Reader::at(setup0, header.records_offset())?;
let counts = header.counts();
let languages = read_n(&mut reader, counts.languages, version, LanguageEntry::read)?;
let messages = read_n(
&mut reader,
counts.custom_messages,
version,
MessageEntry::read,
)?;
let permissions = read_n(
&mut reader,
counts.permissions,
version,
PermissionEntry::read,
)?;
let types = read_n(&mut reader, counts.types, version, TypeEntry::read)?;
let components = read_n(
&mut reader,
counts.components,
version,
ComponentEntry::read,
)?;
let tasks = read_n(&mut reader, counts.tasks, version, TaskEntry::read)?;
let directories = read_n(
&mut reader,
counts.directories,
version,
DirectoryEntry::read,
)?;
let iss_sig_keys = if let Some(n) = counts.iss_sig_keys {
read_n(&mut reader, n, version, ISSigKeyEntry::read)?
} else {
Vec::new()
};
let files = read_n(&mut reader, counts.files, version, FileEntry::read)?;
let icons = read_n(&mut reader, counts.icons, version, IconEntry::read)?;
let ini_entries = read_n(&mut reader, counts.ini_entries, version, IniEntry::read)?;
let registry = read_n(&mut reader, counts.registry, version, RegistryEntry::read)?;
let install_deletes = read_n(
&mut reader,
counts.install_deletes,
version,
DeleteEntry::read,
)?;
let uninstall_deletes = read_n(
&mut reader,
counts.uninstall_deletes,
version,
DeleteEntry::read,
)?;
let run = read_n(&mut reader, counts.run, version, RunEntry::read)?;
let uninstall_run = read_n(&mut reader, counts.uninstall_run, version, RunEntry::read)?;
let mut data_reader = Reader::new(data_block);
let file_locations = read_n(
&mut data_reader,
counts.file_locations,
version,
DataEntry::read,
)?;
Ok(ParsedRecords {
languages,
messages,
permissions,
types,
components,
tasks,
directories,
iss_sig_keys,
files,
icons,
ini_entries,
registry,
install_deletes,
uninstall_deletes,
run,
uninstall_run,
file_locations,
})
}
fn read_n<T, F>(
reader: &mut Reader<'_>,
count: u32,
version: &Version,
mut read_one: F,
) -> Result<Vec<T>, Error>
where
F: FnMut(&mut Reader<'_>, &Version) -> Result<T, Error>,
{
let cap = usize::try_from(count).map_err(|_| Error::Overflow {
what: "record count",
})?;
let mut out = Vec::with_capacity(cap);
for _ in 0..cap {
out.push(read_one(reader, version)?);
}
Ok(out)
}
fn usize_from_u64(value: u64, what: &'static str) -> Result<usize, Error> {
usize::try_from(value).map_err(|_| Error::Overflow { what })
}
fn parse_encryption_header(reader: &mut Reader<'_>) -> Result<Option<EncryptionInfo>, Error> {
let _crc = reader.u32_le("encryption header CRC")?;
let mode_byte = reader.u8("EncryptionUse")?;
let salt = reader.array::<16>("KDFSalt")?;
let kdf_iterations = reader.u32_le("KDFIterations")?;
let base_nonce = reader.array::<24>("BaseNonce")?;
let password_test = reader.u32_le("PasswordTest")?;
let mode = match mode_byte {
0 => EncryptionMode::None,
1 => EncryptionMode::Files,
2 => EncryptionMode::Full,
_ => EncryptionMode::None,
};
Ok(Some(EncryptionInfo {
mode,
salt,
kdf_iterations,
base_nonce,
password_test,
}))
}