mod reader;
use crate::{Encoding, Error, RecognitionFailure, crypto, decompress};
use reader::Reader;
const FILE_MARKER_LEN: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limits {
pub max_records: usize,
pub max_encrypted_blob_size: usize,
pub max_metadata_string_bytes: usize,
pub max_decompressed_blob_size: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
max_records: 256,
max_encrypted_blob_size: 64 * 1024 * 1024,
max_metadata_string_bytes: 1024 * 1024,
max_decompressed_blob_size: 64 * 1024 * 1024,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecompressionStatus {
NotCompressed,
Decompressed,
Failed {
reason: RecognitionFailure,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecordParseDiagnostic {
pub record_index: usize,
pub offset: usize,
pub reason: RecognitionFailure,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecordParseReport {
records: Vec<Record>,
diagnostic: Option<RecordParseDiagnostic>,
}
impl RecordParseReport {
#[must_use]
pub fn records(&self) -> &[Record] {
self.records.as_slice()
}
#[must_use]
pub fn into_records(self) -> Vec<Record> {
self.records
}
#[must_use]
pub const fn diagnostic(&self) -> Option<RecordParseDiagnostic> {
self.diagnostic
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncryptionProfile {
Ea05Mt,
Ea06Lame,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionProfile {
None,
Ea04,
Ea05,
Ea06,
Jb00,
Jb01,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecordProfile {
pub encoding: Encoding,
pub encryption: EncryptionProfile,
pub compression: CompressionProfile,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Record {
index: usize,
offset: usize,
subtype: DecodedString,
name: DecodedString,
compressed: bool,
compressed_size: u32,
uncompressed_size: u32,
checksum: u32,
checksum_valid: bool,
creation_time: u64,
last_write_time: u64,
encrypted_data: Vec<u8>,
decrypted_data: Vec<u8>,
decompressed_data: Option<Vec<u8>>,
decompression_status: DecompressionStatus,
profile: RecordProfile,
}
impl Record {
#[must_use]
pub const fn index(&self) -> usize {
self.index
}
#[must_use]
pub const fn offset(&self) -> usize {
self.offset
}
#[must_use]
pub fn subtype(&self) -> &str {
self.subtype.text()
}
#[must_use]
pub fn subtype_bytes(&self) -> &[u8] {
self.subtype.bytes()
}
#[must_use]
pub fn name(&self) -> &str {
self.name.text()
}
#[must_use]
pub fn name_bytes(&self) -> &[u8] {
self.name.bytes()
}
#[must_use]
pub const fn compressed(&self) -> bool {
self.compressed
}
#[must_use]
pub const fn compressed_size(&self) -> u32 {
self.compressed_size
}
#[must_use]
pub const fn uncompressed_size(&self) -> u32 {
self.uncompressed_size
}
#[must_use]
pub const fn checksum(&self) -> u32 {
self.checksum
}
#[must_use]
pub const fn checksum_valid(&self) -> bool {
self.checksum_valid
}
#[must_use]
pub const fn creation_time(&self) -> u64 {
self.creation_time
}
#[must_use]
pub const fn last_write_time(&self) -> u64 {
self.last_write_time
}
#[must_use]
pub fn encrypted_data(&self) -> &[u8] {
self.encrypted_data.as_slice()
}
#[must_use]
pub fn decrypted_data(&self) -> &[u8] {
self.decrypted_data.as_slice()
}
#[must_use]
pub fn decompressed_data(&self) -> Option<&[u8]> {
self.decompressed_data.as_deref()
}
#[must_use]
pub fn payload_data(&self) -> &[u8] {
match self.decompressed_data.as_deref() {
Some(data) => data,
None => self.decrypted_data.as_slice(),
}
}
#[must_use]
pub const fn decompression_status(&self) -> DecompressionStatus {
self.decompression_status
}
#[must_use]
pub const fn profile(&self) -> RecordProfile {
self.profile
}
#[cfg(test)]
pub fn from_parts_for_test(parts: RecordTestParts) -> Self {
Self {
index: parts.index,
offset: parts.offset,
subtype: parts.subtype,
name: parts.name,
compressed: parts.compressed,
compressed_size: parts.compressed_size,
uncompressed_size: parts.uncompressed_size,
checksum: parts.checksum,
checksum_valid: parts.checksum_valid,
creation_time: parts.creation_time,
last_write_time: parts.last_write_time,
encrypted_data: parts.encrypted_data,
decrypted_data: parts.decrypted_data,
decompressed_data: parts.decompressed_data,
decompression_status: parts.decompression_status,
profile: parts.profile,
}
}
}
#[cfg(test)]
#[derive(Debug, Clone)]
pub struct RecordTestParts {
pub index: usize,
pub offset: usize,
pub subtype: DecodedString,
pub name: DecodedString,
pub compressed: bool,
pub compressed_size: u32,
pub uncompressed_size: u32,
pub checksum: u32,
pub checksum_valid: bool,
pub creation_time: u64,
pub last_write_time: u64,
pub encrypted_data: Vec<u8>,
pub decrypted_data: Vec<u8>,
pub decompressed_data: Option<Vec<u8>>,
pub decompression_status: DecompressionStatus,
pub profile: RecordProfile,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedString {
raw: Vec<u8>,
text: String,
}
impl DecodedString {
fn new(raw: Vec<u8>, text: String) -> Self {
Self { raw, text }
}
#[cfg(test)]
pub fn from_text_for_test(text: &str) -> Self {
Self {
raw: text.as_bytes().to_vec(),
text: text.to_string(),
}
}
#[must_use]
pub fn text(&self) -> &str {
self.text.as_str()
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
self.raw.as_slice()
}
}
#[derive(Debug, Clone, Copy)]
struct Profile {
encoding: Encoding,
file_key: u32,
subtype_len_xor: u32,
subtype_key_base: u32,
name_len_xor: u32,
name_key_base: u32,
size_xor: u32,
checksum_xor: u32,
data_key_base: u32,
has_checksum: bool,
}
impl Profile {
fn for_encoding(encoding: Encoding) -> Result<Self, Error> {
match encoding {
Encoding::Ea04 => Ok(Self {
encoding,
file_key: 0x16fa,
subtype_len_xor: 0x29bc,
subtype_key_base: 0xa25e,
name_len_xor: 0x29ac,
name_key_base: 0xf25e,
size_xor: 0x45aa,
checksum_xor: 0,
data_key_base: 0x22af,
has_checksum: false,
}),
Encoding::Ea05 => Ok(Self {
encoding,
file_key: 0x16fa,
subtype_len_xor: 0x29bc,
subtype_key_base: 0xa25e,
name_len_xor: 0x29ac,
name_key_base: 0xf25e,
size_xor: 0x45aa,
checksum_xor: 0xc3d2,
data_key_base: 0x22af,
has_checksum: true,
}),
Encoding::Ea06 => Ok(Self {
encoding,
file_key: 0x18ee,
subtype_len_xor: 0xadbc,
subtype_key_base: 0xb33f,
name_len_xor: 0xf820,
name_key_base: 0xf479,
size_xor: 0x87bc,
checksum_xor: 0xa685,
data_key_base: 0x2477,
has_checksum: true,
}),
Encoding::Jb01 => Ok(Self {
encoding,
file_key: 0x16fa,
subtype_len_xor: 0x29bc,
subtype_key_base: 0xa25e,
name_len_xor: 0x29ac,
name_key_base: 0xf25e,
size_xor: 0x45aa,
checksum_xor: 0,
data_key_base: 0x22af,
has_checksum: false,
}),
}
}
fn decrypt(self, data: &[u8], key: u32) -> Result<Vec<u8>, Error> {
match self.encoding {
Encoding::Ea04 | Encoding::Ea05 | Encoding::Jb01 => {
crypto::mt::decrypt(data, key).ok_or_else(Error::crypto_mismatch)
}
Encoding::Ea06 => crypto::lame::decrypt(data, key).ok_or_else(Error::crypto_mismatch),
}
}
fn character_width(self) -> usize {
match self.encoding {
Encoding::Ea04 | Encoding::Ea05 | Encoding::Jb01 => 1,
Encoding::Ea06 => 2,
}
}
fn encryption_profile(self) -> EncryptionProfile {
match self.encoding {
Encoding::Ea04 | Encoding::Ea05 | Encoding::Jb01 => EncryptionProfile::Ea05Mt,
Encoding::Ea06 => EncryptionProfile::Ea06Lame,
}
}
fn data_key(self, checksum: u32) -> u32 {
match self.encoding {
Encoding::Ea04 | Encoding::Ea05 | Encoding::Jb01 => {
checksum.wrapping_add(self.data_key_base)
}
Encoding::Ea06 => self.data_key_base,
}
}
}
pub fn parse_records(
data: &[u8],
offset: usize,
encoding: Encoding,
limits: Limits,
) -> Result<Vec<Record>, Error> {
let report = parse_records_partial(data, offset, encoding, limits)?;
if let Some(diagnostic) = report.diagnostic() {
return Err(error_from_failure(diagnostic.reason));
}
Ok(report.into_records())
}
pub fn parse_records_partial(
data: &[u8],
offset: usize,
encoding: Encoding,
limits: Limits,
) -> Result<RecordParseReport, Error> {
let profile = Profile::for_encoding(encoding)?;
let mut reader = Reader::new(data, offset);
let mut records = Vec::new();
let data_key_salt = match encoding {
Encoding::Ea04 | Encoding::Jb01 => 0,
_ => ea05_data_key_salt(&reader, offset),
};
while records.len() < limits.max_records {
let index = records.len();
let record_offset = reader.position();
let parse_result = parse_one(&mut reader, profile, index, limits, data_key_salt);
let Some(record) = (match parse_result {
Ok(record) => record,
Err(err) => {
return Ok(RecordParseReport {
records,
diagnostic: Some(RecordParseDiagnostic {
record_index: index,
offset: record_offset,
reason: failure_from_error(&err),
}),
});
}
}) else {
return Ok(RecordParseReport {
records,
diagnostic: None,
});
};
records.push(record);
}
Ok(RecordParseReport {
records,
diagnostic: Some(RecordParseDiagnostic {
record_index: limits.max_records,
offset: reader.position(),
reason: RecognitionFailure::LimitExceeded,
}),
})
}
fn failure_from_error(err: &Error) -> RecognitionFailure {
match err.recognition_failure() {
Some(reason) => reason,
None => RecognitionFailure::MalformedContainer,
}
}
fn error_from_failure(failure: RecognitionFailure) -> Error {
match failure {
RecognitionFailure::NotRecognized => Error::not_recognized(),
RecognitionFailure::UnsupportedEncoding => Error::unsupported_encoding(),
RecognitionFailure::MalformedContainer => Error::malformed_container(),
RecognitionFailure::Truncated => Error::truncated(),
RecognitionFailure::LimitExceeded => Error::limit_exceeded(),
RecognitionFailure::CryptoMismatch => Error::crypto_mismatch(),
RecognitionFailure::CompressionError => Error::compression_error(),
RecognitionFailure::TokenError => Error::token_error(),
}
}
fn parse_one(
reader: &mut Reader<'_>,
profile: Profile,
index: usize,
limits: Limits,
data_key_salt: u32,
) -> Result<Option<Record>, Error> {
if reader.remaining() == 0 {
return Ok(None);
}
let offset = reader.position();
let marker = reader.read_bytes(FILE_MARKER_LEN)?;
let decrypted_marker = profile.decrypt(marker, profile.file_key)?;
if decrypted_marker != b"FILE" {
return Ok(None);
}
let subtype_len = reader.read_u32_le()? ^ profile.subtype_len_xor;
let subtype = read_string(
reader,
profile,
subtype_len,
profile.subtype_key_base,
limits.max_metadata_string_bytes,
)?;
let name_len = reader.read_u32_le()? ^ profile.name_len_xor;
let name = read_string(
reader,
profile,
name_len,
profile.name_key_base,
limits.max_metadata_string_bytes,
)?;
let compressed = reader.read_u8()? != 0;
let compressed_size = reader.read_u32_le()? ^ profile.size_xor;
let uncompressed_size = reader.read_u32_le()? ^ profile.size_xor;
let checksum = if profile.has_checksum {
reader.read_u32_le()? ^ profile.checksum_xor
} else {
0
};
let creation_time = reader.read_u64_le()?;
let last_write_time = reader.read_u64_le()?;
let encrypted_len = usize::try_from(compressed_size).map_err(|_err| Error::limit_exceeded())?;
if encrypted_len > limits.max_encrypted_blob_size {
return Err(Error::limit_exceeded());
}
let encrypted_data = reader.read_bytes(encrypted_len)?.to_vec();
let decrypted_data =
profile.decrypt(encrypted_data.as_slice(), profile.data_key(data_key_salt))?;
let checksum_valid = !profile.has_checksum
|| adler32(decrypted_data.as_slice()).is_some_and(|actual| actual == checksum);
let (decompressed_data, decompression_status) = maybe_decompress(
decrypted_data.as_slice(),
compressed,
limits.max_decompressed_blob_size,
);
let record_profile = RecordProfile {
encoding: profile.encoding,
encryption: profile.encryption_profile(),
compression: compression_profile(decrypted_data.as_slice(), compressed),
};
Ok(Some(Record {
index,
offset,
subtype,
name,
compressed,
compressed_size,
uncompressed_size,
checksum,
checksum_valid,
creation_time,
last_write_time,
encrypted_data,
decrypted_data,
decompressed_data,
decompression_status,
profile: record_profile,
}))
}
fn maybe_decompress(
data: &[u8],
compressed: bool,
max_output_size: usize,
) -> (Option<Vec<u8>>, DecompressionStatus) {
if !compressed {
return (None, DecompressionStatus::NotCompressed);
}
match decompress::decompress(data, decompress::Limits { max_output_size }) {
Ok(bytes) => (Some(bytes), DecompressionStatus::Decompressed),
Err(err) => {
let reason = match err.recognition_failure() {
Some(reason) => reason,
None => RecognitionFailure::CompressionError,
};
(None, DecompressionStatus::Failed { reason })
}
}
}
fn ea05_data_key_salt(reader: &Reader<'_>, record_offset: usize) -> u32 {
let Some(marker_start) = record_offset.checked_sub(20) else {
return 0;
};
let Some(marker_end) = marker_start.checked_add(4) else {
return 0;
};
if reader.range(marker_start, marker_end) != Some(b"EA05") {
return 0;
}
let Some(salt_start) = record_offset.checked_sub(16) else {
return 0;
};
reader
.range(salt_start, record_offset)
.map_or(0, |bytes| bytes.iter().map(|byte| u32::from(*byte)).sum())
}
fn compression_profile(data: &[u8], compressed: bool) -> CompressionProfile {
if !compressed {
return CompressionProfile::None;
}
match data.get(0..4) {
Some(magic) if magic == b"EA04" => CompressionProfile::Ea04,
Some(magic) if magic == b"EA05" => CompressionProfile::Ea05,
Some(magic) if magic == b"EA06" => CompressionProfile::Ea06,
Some(magic) if magic == b"JB00" => CompressionProfile::Jb00,
Some(magic) if magic == b"JB01" => CompressionProfile::Jb01,
_ => CompressionProfile::Unknown,
}
}
fn read_string(
reader: &mut Reader<'_>,
profile: Profile,
char_len: u32,
key_base: u32,
max_bytes: usize,
) -> Result<DecodedString, Error> {
let chars = usize::try_from(char_len).map_err(|_err| Error::limit_exceeded())?;
let byte_len = chars
.checked_mul(profile.character_width())
.ok_or_else(Error::limit_exceeded)?;
if byte_len > max_bytes {
return Err(Error::limit_exceeded());
}
let encrypted = reader.read_bytes(byte_len)?;
let key = key_base.wrapping_add(char_len);
let raw = profile.decrypt(encrypted, key)?;
let text = match profile.encoding {
Encoding::Ea04 | Encoding::Ea05 | Encoding::Jb01 => {
String::from_utf8_lossy(raw.as_slice()).into_owned()
}
Encoding::Ea06 => decode_utf16_lossy(raw.as_slice())?,
};
Ok(DecodedString::new(raw, text))
}
fn decode_utf16_lossy(data: &[u8]) -> Result<String, Error> {
let chunks = data.chunks_exact(2);
if !chunks.remainder().is_empty() {
return Err(Error::truncated());
}
let code_units: Vec<u16> = chunks
.map(|chunk| {
let bytes: [u8; 2] = chunk.try_into().map_err(|_err| Error::truncated())?;
Ok(u16::from_le_bytes(bytes))
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(String::from_utf16_lossy(code_units.as_slice()))
}
fn adler32(data: &[u8]) -> Option<u32> {
const MOD_ADLER: u32 = 65_521;
let mut a = 1u32;
let mut b = 0u32;
for byte in data {
a = a.checked_add(u32::from(*byte))? % MOD_ADLER;
b = b.checked_add(a)? % MOD_ADLER;
}
Some((b << 16) | a)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_ea06_record() -> Result<(), String> {
let mut data = Vec::new();
append_ea06_record(
&mut data,
">>>AUTOIT SCRIPT<<<",
"main.au3",
false,
0x0102_0304_0506_0708,
0x1112_1314_1516_1718,
b"payload",
)?;
let records = parse_records(data.as_slice(), 0, Encoding::Ea06, Limits::default())
.map_err(|err| err.to_string())?;
let record = records
.first()
.ok_or_else(|| "missing first record".to_string())?;
check_eq(records.len(), 1, "record count")?;
check_eq(record.index(), 0, "record index")?;
check_eq(record.offset(), 0, "record offset")?;
check_eq(record.subtype(), ">>>AUTOIT SCRIPT<<<", "subtype")?;
check_eq(record.name(), "main.au3", "name")?;
check_eq(record.compressed(), false, "compressed")?;
check_eq(record.compressed_size(), 7, "compressed size")?;
check_eq(record.uncompressed_size(), 7, "uncompressed size")?;
check_eq(record.checksum_valid(), true, "checksum")?;
check_eq(record.creation_time(), 0x0102_0304_0506_0708, "created")?;
check_eq(record.last_write_time(), 0x1112_1314_1516_1718, "modified")?;
check_eq(
record.profile(),
RecordProfile {
encoding: Encoding::Ea06,
encryption: EncryptionProfile::Ea06Lame,
compression: CompressionProfile::None,
},
"profile",
)?;
check_eq(record.decrypted_data(), b"payload".as_slice(), "data")
}
#[test]
fn parses_ea05_record() -> Result<(), String> {
let mut data = Vec::new();
append_ea05_record(
&mut data,
">AUTOIT SCRIPT<",
"legacy.au3",
false,
0,
0,
b"legacy",
)?;
let records = parse_records(data.as_slice(), 0, Encoding::Ea05, Limits::default())
.map_err(|err| err.to_string())?;
let record = records
.first()
.ok_or_else(|| "missing first record".to_string())?;
check_eq(records.len(), 1, "record count")?;
check_eq(record.subtype(), ">AUTOIT SCRIPT<", "subtype")?;
check_eq(record.name(), "legacy.au3", "name")?;
check_eq(record.compressed(), false, "compressed")?;
check_eq(record.checksum_valid(), true, "checksum")?;
check_eq(
record.profile(),
RecordProfile {
encoding: Encoding::Ea05,
encryption: EncryptionProfile::Ea05Mt,
compression: CompressionProfile::None,
},
"profile",
)?;
check_eq(record.decrypted_data(), b"legacy".as_slice(), "data")
}
#[test]
fn stores_decompressed_payload_for_compressed_record() -> Result<(), String> {
let compressed = ea06_literal_blob(b"ABC")?;
let mut data = Vec::new();
append_ea06_record(
&mut data,
">AUTOIT SCRIPT<",
"compressed.au3",
true,
0,
0,
compressed.as_slice(),
)?;
let records = parse_records(data.as_slice(), 0, Encoding::Ea06, Limits::default())
.map_err(|err| err.to_string())?;
let record = records
.first()
.ok_or_else(|| "missing first record".to_string())?;
check_eq(
record.decompression_status(),
DecompressionStatus::Decompressed,
"decompression status",
)?;
check_eq(
record.decompressed_data(),
Some(b"ABC".as_slice()),
"decompressed data",
)?;
check_eq(
record.profile().compression,
CompressionProfile::Ea06,
"compression profile",
)?;
check_eq(record.payload_data(), b"ABC".as_slice(), "payload data")
}
#[test]
fn partial_parser_preserves_records_before_truncated_record() -> Result<(), String> {
let mut data = Vec::new();
append_ea06_record(&mut data, ">AUTOIT SCRIPT<", "ok.au3", false, 0, 0, b"ok")?;
let truncated_offset = data.len();
append_encrypted(&mut data, b"FILE", Encoding::Ea06, 0x18ee)?;
let report = parse_records_partial(data.as_slice(), 0, Encoding::Ea06, Limits::default())
.map_err(|err| err.to_string())?;
check_eq(report.records().len(), 1, "record count")?;
check_eq(
report.diagnostic(),
Some(RecordParseDiagnostic {
record_index: 1,
offset: truncated_offset,
reason: RecognitionFailure::Truncated,
}),
"diagnostic",
)?;
let Err(err) = parse_records(data.as_slice(), 0, Encoding::Ea06, Limits::default()) else {
return Err("strict parser unexpectedly succeeded".to_string());
};
check_eq(
err.recognition_failure(),
Some(RecognitionFailure::Truncated),
"strict error",
)
}
fn append_ea06_record(
out: &mut Vec<u8>,
subtype: &str,
name: &str,
compressed: bool,
creation_time: u64,
last_write_time: u64,
data: &[u8],
) -> Result<(), String> {
append_encrypted(out, b"FILE", Encoding::Ea06, 0x18ee)?;
append_xored_u32(out, utf16_len(subtype)?, 0xadbc);
append_encrypted_utf16(out, subtype, 0xb33f)?;
append_xored_u32(out, utf16_len(name)?, 0xf820);
append_encrypted_utf16(out, name, 0xf479)?;
out.push(if compressed { 1 } else { 0 });
let data_len = u32::try_from(data.len()).map_err(|err| err.to_string())?;
append_xored_u32(out, data_len, 0x87bc);
append_xored_u32(out, data_len, 0x87bc);
append_xored_u32(
out,
adler32(data).ok_or_else(|| "adler failed".to_string())?,
0xa685,
);
append_u64(out, creation_time);
append_u64(out, last_write_time);
append_encrypted(out, data, Encoding::Ea06, 0x2477)
}
fn ea06_literal_blob(data: &[u8]) -> Result<Vec<u8>, String> {
let mut blob = Vec::from(*b"EA06");
let len = u32::try_from(data.len()).map_err(|err| err.to_string())?;
blob.extend_from_slice(&len.to_be_bytes());
let mut bits = Vec::new();
for byte in data {
bits.push(1);
for shift in (0..8u8).rev() {
bits.push((byte >> shift) & 1);
}
}
blob.extend_from_slice(pack_bits(bits.as_slice())?.as_slice());
Ok(blob)
}
fn pack_bits(bits: &[u8]) -> Result<Vec<u8>, String> {
let mut out = Vec::new();
let mut cursor = 0usize;
while cursor < bits.len() {
let mut byte = 0u8;
for bit_index in 0..8usize {
let source_index = cursor
.checked_add(bit_index)
.ok_or_else(|| "bit offset overflow".to_string())?;
let bit = bits
.get(source_index)
.copied()
.map_or(0, core::convert::identity);
byte = (byte << 1) | bit;
}
out.push(byte);
cursor = cursor
.checked_add(8)
.ok_or_else(|| "bit offset overflow".to_string())?;
}
Ok(out)
}
fn append_ea05_record(
out: &mut Vec<u8>,
subtype: &str,
name: &str,
compressed: bool,
creation_time: u64,
last_write_time: u64,
data: &[u8],
) -> Result<(), String> {
append_encrypted(out, b"FILE", Encoding::Ea05, 0x16fa)?;
append_xored_u32(out, byte_len(subtype)?, 0x29bc);
append_encrypted(
out,
subtype.as_bytes(),
Encoding::Ea05,
0xa25e_u32.wrapping_add(byte_len(subtype)?),
)?;
append_xored_u32(out, byte_len(name)?, 0x29ac);
append_encrypted(
out,
name.as_bytes(),
Encoding::Ea05,
0xf25e_u32.wrapping_add(byte_len(name)?),
)?;
out.push(if compressed { 1 } else { 0 });
let data_len = u32::try_from(data.len()).map_err(|err| err.to_string())?;
append_xored_u32(out, data_len, 0x45aa);
append_xored_u32(out, data_len, 0x45aa);
append_xored_u32(
out,
adler32(data).ok_or_else(|| "adler failed".to_string())?,
0xc3d2,
);
append_u64(out, creation_time);
append_u64(out, last_write_time);
append_encrypted(out, data, Encoding::Ea05, 0x22af)
}
fn append_encrypted_utf16(out: &mut Vec<u8>, value: &str, key_base: u32) -> Result<(), String> {
let char_len = utf16_len(value)?;
let key = key_base.wrapping_add(char_len);
let mut bytes = Vec::new();
for unit in value.encode_utf16() {
bytes.extend_from_slice(&unit.to_le_bytes());
}
append_encrypted(out, bytes.as_slice(), Encoding::Ea06, key)
}
fn append_encrypted(
out: &mut Vec<u8>,
plain: &[u8],
encoding: Encoding,
key: u32,
) -> Result<(), String> {
let encrypted = match encoding {
Encoding::Ea04 | Encoding::Ea05 | Encoding::Jb01 => {
crate::crypto::mt::decrypt(plain, key)
}
Encoding::Ea06 => crate::crypto::lame::decrypt(plain, key),
}
.ok_or_else(|| "encryption failed".to_string())?;
out.extend_from_slice(encrypted.as_slice());
Ok(())
}
fn append_xored_u32(out: &mut Vec<u8>, value: u32, mask: u32) {
append_u32(out, value ^ mask);
}
fn append_u32(out: &mut Vec<u8>, value: u32) {
out.extend_from_slice(&value.to_le_bytes());
}
fn append_u64(out: &mut Vec<u8>, value: u64) {
out.extend_from_slice(&value.to_le_bytes());
}
fn utf16_len(value: &str) -> Result<u32, String> {
u32::try_from(value.encode_utf16().count()).map_err(|err| err.to_string())
}
fn byte_len(value: &str) -> Result<u32, String> {
u32::try_from(value.len()).map_err(|err| err.to_string())
}
fn check_eq<T>(actual: T, expected: T, context: &str) -> Result<(), String>
where
T: core::fmt::Debug + PartialEq,
{
if actual == expected {
Ok(())
} else {
Err(format!("{context}: got {actual:?}, expected {expected:?}"))
}
}
}