use super::compress::{decompress_raw_zlib, decompress_zstd_frames, CompressError};
use super::crypto::{tea_decrypt_in_place, CryptoError, EcdhTeaCipher};
use super::protocol::{
MAGIC_ASYNC_NO_CRYPT_ZLIB_START, MAGIC_ASYNC_NO_CRYPT_ZSTD_START, MAGIC_ASYNC_ZLIB_START,
MAGIC_ASYNC_ZSTD_START, MAGIC_END, MAGIC_SYNC_NO_CRYPT_ZLIB_START,
MAGIC_SYNC_NO_CRYPT_ZSTD_START, MAGIC_SYNC_ZLIB_START, MAGIC_SYNC_ZSTD_START,
};
use hex::FromHexError;
use thiserror::Error;
const BASE_KEY: u8 = 0xcc;
const LEGACY_MAGIC_CRYPT_START: u8 = 0x01;
const LEGACY_MAGIC_COMPRESS_CRYPT_START: u8 = 0x02;
const LEGACY_NEW_MAGIC_CRYPT_START: u8 = 0x03;
const LEGACY_NEW_MAGIC_COMPRESS_CRYPT_START: u8 = 0x04;
const LEGACY_NEW_MAGIC_COMPRESS_CRYPT_START1: u8 = 0x05;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DecodeSummary {
pub blocks: usize,
pub output_bytes: usize,
}
#[derive(Debug, Clone, Default)]
pub struct DecodeOptions {
private_key: Option<[u8; 32]>,
}
impl DecodeOptions {
pub fn new() -> Self {
Self::default()
}
pub fn private_key(mut self, private_key: [u8; 32]) -> Self {
self.private_key = Some(private_key);
self
}
pub fn private_key_hex(self, private_key_hex: &str) -> Result<Self, DecodeError> {
Ok(self.private_key(parse_private_key_hex(private_key_hex)?))
}
}
#[derive(Debug, Error)]
pub enum DecodeError {
#[error("private key must be 64 hex chars")]
InvalidPrivateKeyLength,
#[error("invalid private key hex: {0}")]
InvalidPrivateKeyHex(#[from] FromHexError),
#[error("invalid secp256k1 key material")]
InvalidKeyMaterial,
#[error("no valid xlog block found")]
NoValidBlock,
#[error("encrypted xlog block requires --key/--private-key")]
MissingPrivateKey,
#[error("truncated xlog block at offset {offset}: payload len {len}")]
TruncatedBlock {
offset: usize,
len: usize,
},
#[error("invalid xlog tail marker at offset {offset}")]
InvalidTail {
offset: usize,
},
#[error(transparent)]
Compress(#[from] CompressError),
#[error(transparent)]
Crypto(CryptoError),
}
impl From<CryptoError> for DecodeError {
fn from(value: CryptoError) -> Self {
match value {
CryptoError::InvalidKeyMaterial => Self::InvalidKeyMaterial,
other => Self::Crypto(other),
}
}
}
#[derive(Debug, Clone)]
struct BlockHeader {
magic: u8,
header_len: usize,
len: usize,
seq: u16,
client_pubkey: [u8; 64],
}
pub fn decode_xlog(
input: &[u8],
output: &mut Vec<u8>,
opts: &DecodeOptions,
) -> Result<DecodeSummary, DecodeError> {
let Some(mut offset) = find_log_start(input, 2) else {
return Err(DecodeError::NoValidBlock);
};
let mut blocks = 0usize;
let mut last_seq = 0u16;
loop {
if offset >= input.len() {
break;
}
if !is_good_log_buffer(input, offset, 1) {
let Some(fix_pos) = find_log_start(&input[offset..], 1) else {
break;
};
output.extend_from_slice(
format!("[F]decode_log_file.py decode error len={fix_pos}\n").as_bytes(),
);
offset += fix_pos;
}
let header = parse_header(input, offset)?;
let payload_start = offset + header.header_len;
let payload_end = payload_start + header.len;
if payload_end + 1 > input.len() {
return Err(DecodeError::TruncatedBlock {
offset,
len: header.len,
});
}
if input[payload_end] != MAGIC_END {
return Err(DecodeError::InvalidTail {
offset: payload_end,
});
}
if header.seq != 0
&& header.seq != 1
&& last_seq != 0
&& header.seq != last_seq.wrapping_add(1)
{
output.extend_from_slice(
format!(
"[F]decode_log_file.py log seq:{}-{} is missing\n",
last_seq.wrapping_add(1),
header.seq.wrapping_sub(1)
)
.as_bytes(),
);
}
if header.seq != 0 {
last_seq = header.seq;
}
let payload = &input[payload_start..payload_end];
let decoded = decode_payload(&header, payload, opts)?;
output.extend_from_slice(&decoded);
blocks += 1;
offset = payload_end + 1;
}
if blocks == 0 {
return Err(DecodeError::NoValidBlock);
}
Ok(DecodeSummary {
blocks,
output_bytes: output.len(),
})
}
pub fn parse_private_key_hex(private_key_hex: &str) -> Result<[u8; 32], DecodeError> {
let trimmed = private_key_hex.trim();
let trimmed = trimmed.strip_prefix("0x").unwrap_or(trimmed);
if trimmed.len() != 64 {
return Err(DecodeError::InvalidPrivateKeyLength);
}
let mut out = [0u8; 32];
hex::decode_to_slice(trimmed, &mut out)?;
Ok(out)
}
fn decode_payload(
header: &BlockHeader,
payload: &[u8],
opts: &DecodeOptions,
) -> Result<Vec<u8>, DecodeError> {
match header.magic {
LEGACY_MAGIC_CRYPT_START | LEGACY_NEW_MAGIC_CRYPT_START => {
Ok(xor_payload(payload, legacy_xor_key(header)))
}
LEGACY_MAGIC_COMPRESS_CRYPT_START | LEGACY_NEW_MAGIC_COMPRESS_CRYPT_START => {
let decoded = xor_payload(payload, legacy_xor_key(header));
Ok(decompress_raw_zlib(&decoded)?)
}
LEGACY_NEW_MAGIC_COMPRESS_CRYPT_START1 => {
let joined = join_legacy_compressed_segments(payload);
let decoded = xor_payload(&joined, legacy_xor_key(header));
Ok(decompress_raw_zlib(&decoded)?)
}
MAGIC_SYNC_ZLIB_START
| MAGIC_SYNC_NO_CRYPT_ZLIB_START
| MAGIC_SYNC_ZSTD_START
| MAGIC_SYNC_NO_CRYPT_ZSTD_START => Ok(payload.to_vec()),
MAGIC_ASYNC_ZLIB_START | MAGIC_ASYNC_ZSTD_START => {
let private_key = opts.private_key.ok_or(DecodeError::MissingPrivateKey)?;
let cipher =
EcdhTeaCipher::new_with_server_private_key(header.client_pubkey, private_key)?;
let mut decoded = payload.to_vec();
let block_end = decoded.len() / 8 * 8;
tea_decrypt_in_place(&mut decoded[..block_end], &cipher.tea_key_words());
match header.magic {
MAGIC_ASYNC_ZLIB_START => Ok(decompress_raw_zlib(&decoded)?),
MAGIC_ASYNC_ZSTD_START => Ok(decompress_zstd_frames(&decoded)?),
_ => unreachable!(),
}
}
MAGIC_ASYNC_NO_CRYPT_ZLIB_START => Ok(decompress_raw_zlib(payload)?),
MAGIC_ASYNC_NO_CRYPT_ZSTD_START => Ok(decompress_zstd_frames(payload)?),
_ => Err(DecodeError::NoValidBlock),
}
}
fn parse_header(input: &[u8], offset: usize) -> Result<BlockHeader, DecodeError> {
let magic = input
.get(offset)
.copied()
.ok_or(DecodeError::NoValidBlock)?;
let Some((header_len, crypt_key_len)) = header_lengths_for_magic(magic) else {
return Err(DecodeError::NoValidBlock);
};
if offset + header_len > input.len() {
return Err(DecodeError::TruncatedBlock { offset, len: 0 });
}
let len_offset = offset + header_len - crypt_key_len - 4;
let len = u32::from_le_bytes([
input[len_offset],
input[len_offset + 1],
input[len_offset + 2],
input[len_offset + 3],
]) as usize;
let seq = if header_len >= 1 + 2 + 1 + 1 + 4 {
let seq_offset = offset + header_len - crypt_key_len - 4 - 2 - 2;
u16::from_le_bytes([input[seq_offset], input[seq_offset + 1]])
} else {
0
};
let mut client_pubkey = [0u8; 64];
if crypt_key_len == 64 {
let key_start = offset + header_len - crypt_key_len;
client_pubkey.copy_from_slice(&input[key_start..key_start + 64]);
}
Ok(BlockHeader {
magic,
header_len,
len,
seq,
client_pubkey,
})
}
fn header_lengths_for_magic(magic: u8) -> Option<(usize, usize)> {
match magic {
LEGACY_MAGIC_CRYPT_START | LEGACY_MAGIC_COMPRESS_CRYPT_START => Some((1 + 4, 0)),
LEGACY_NEW_MAGIC_CRYPT_START
| LEGACY_NEW_MAGIC_COMPRESS_CRYPT_START
| LEGACY_NEW_MAGIC_COMPRESS_CRYPT_START1 => Some((1 + 2 + 1 + 1 + 4, 0)),
MAGIC_SYNC_ZLIB_START
| MAGIC_SYNC_NO_CRYPT_ZLIB_START
| MAGIC_ASYNC_ZLIB_START
| MAGIC_ASYNC_NO_CRYPT_ZLIB_START
| MAGIC_SYNC_ZSTD_START
| MAGIC_SYNC_NO_CRYPT_ZSTD_START
| MAGIC_ASYNC_ZSTD_START
| MAGIC_ASYNC_NO_CRYPT_ZSTD_START => Some((1 + 2 + 1 + 1 + 4 + 64, 64)),
_ => None,
}
}
fn is_good_log_buffer(input: &[u8], offset: usize, count: usize) -> bool {
if offset == input.len() {
return true;
}
if count == 0 {
return true;
}
let Ok(header) = parse_header(input, offset) else {
return false;
};
let Some(payload_end) = offset
.checked_add(header.header_len)
.and_then(|value| value.checked_add(header.len))
else {
return false;
};
if payload_end + 1 > input.len() || input[payload_end] != MAGIC_END {
return false;
}
if count <= 1 {
return true;
}
is_good_log_buffer(input, payload_end + 1, count - 1)
}
fn find_log_start(input: &[u8], count: usize) -> Option<usize> {
let mut offset = 0usize;
while offset < input.len() {
if header_lengths_for_magic(input[offset]).is_some()
&& is_good_log_buffer(input, offset, count)
{
return Some(offset);
}
offset += 1;
}
None
}
fn legacy_xor_key(header: &BlockHeader) -> u8 {
match header.magic {
LEGACY_MAGIC_CRYPT_START | LEGACY_MAGIC_COMPRESS_CRYPT_START => {
BASE_KEY ^ (header.len as u8) ^ header.magic
}
_ => BASE_KEY ^ (header.seq as u8) ^ header.magic,
}
}
fn xor_payload(payload: &[u8], key: u8) -> Vec<u8> {
payload.iter().map(|byte| byte ^ key).collect()
}
fn join_legacy_compressed_segments(payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(payload.len());
let mut offset = 0usize;
while offset + 2 <= payload.len() {
let len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
offset += 2;
let end = offset.saturating_add(len).min(payload.len());
out.extend_from_slice(&payload[offset..end]);
offset = end;
}
out
}