use super::super::{FileHashOption, PathHashOption};
use super::*;
use crate::ext::atomic::AtomicQuick;
use crate::ext::mutex::MutexExt;
use crate::utils::files::*;
use crate::utils::struct_pack::*;
use anyhow::Result;
use chacha20::cipher::array::Array;
use chacha20::hchacha;
use chacha20::{ChaCha20Legacy, KeyIvInit};
use msg_tool_macro::{MyDebug, StructUnpack};
use pelite::PeFile;
use serde::{Deserializer, Serialize, Serializer, de};
use sha3::Sha3_224;
use shake::Shake256;
use std::collections::HashSet;
use std::ops::{Deref, DerefMut, Index};
use std::path::PathBuf;
use std::sync::atomic::AtomicU64;
use std::sync::{Mutex, Weak};
use tjs2dec::{Tjs2File, Tjs2Object};

const S_CTL_BLOCK_SIGNATURE: &[u8] = b" Encryption control block";

macro_rules! base_schema_impl {
    () => {
        fn hash_after_crypt(&self) -> bool {
            AsRef::<BaseSchema>::as_ref(self.as_ref()).hash_after_crypt
        }
        fn startup_tjs_not_encrypted(&self) -> bool {
            AsRef::<BaseSchema>::as_ref(self.as_ref()).startup_tjs_not_encrypted
        }
        fn obfuscated_index(&self) -> bool {
            AsRef::<BaseSchema>::as_ref(self.as_ref()).obfuscated_index
        }
    };
}

#[derive(Debug)]
pub struct CxEncryption {
    mask: u32,
    offset: u32,
    prolog_order: Vec<u8>,
    odd_branch_order: Vec<u8>,
    even_branch_order: Vec<u8>,
    control_block: Arc<Vec<u32>>,
    programs: Vec<Box<dyn ICxProgram + Send + Sync>>,
    program_builder: Box<dyn ICxProgramBuilder + Send + Sync>,
    base: BaseSchema,
}

trait ICxEncryption: std::fmt::Debug {
    fn get_base_offset(&self, hash: u32) -> u32;
    fn inner_decrypt(
        &self,
        mut key: u32,
        mut offset: u64,
        buffer: &mut [u8],
        mut pos: usize,
        mut count: usize,
    ) -> Result<()> {
        let base_offset = self.get_base_offset(key);
        if offset < base_offset as u64 {
            let base_length = ((base_offset as u64 - offset) as usize).min(count);
            self.decode(key, offset, buffer, pos, base_length)?;
            offset += base_length as u64;
            pos += base_length;
            count -= base_length;
        }
        if count > 0 {
            key = (key >> 16) ^ key;
            self.decode(key, offset, buffer, pos, count)?;
        }
        Ok(())
    }
    fn decode(
        &self,
        key: u32,
        offset: u64,
        buffer: &mut [u8],
        pos: usize,
        count: usize,
    ) -> Result<()>;
}

impl CxEncryption {
    pub fn new(base: BaseSchema, schema: &CxSchema, filename: &str) -> Result<Arc<Self>> {
        Ok(Arc::new(Self::new_inner(
            base,
            schema,
            filename,
            Box::new(CxProgramBuilder::default()),
        )?))
    }
    fn new_inner(
        base: BaseSchema,
        schema: &CxSchema,
        filename: &str,
        program_builder: Box<dyn ICxProgramBuilder + Send + Sync>,
    ) -> Result<Self> {
        let control_block = if let Some(tpm_path) = &schema.tpm_file_name {
            Self::read_tpm(tpm_path, filename)?
        } else if let Some(control_block_name) = &schema.control_block_name {
            CX_CB_TABLE
                .get(control_block_name)
                .ok_or_else(|| {
                    anyhow::anyhow!(
                        "Control block not found in cx_cb.pck: {}",
                        control_block_name
                    )
                })?
                .clone()
        } else {
            return Err(anyhow::anyhow!(
                "TPM file name or control block is required in schema"
            ));
        };
        Self::new_inner2(base, schema, program_builder, control_block)
    }

    fn new_inner2(
        base: BaseSchema,
        schema: &CxSchema,
        program_builder: Box<dyn ICxProgramBuilder + Send + Sync>,
        control_block: Vec<u32>,
    ) -> Result<Self> {
        if schema.prolog_order.len() != 3 {
            return Err(anyhow::anyhow!("Prolog order must have 3 elements"));
        }
        if schema.odd_branch_order.len() != 6 {
            return Err(anyhow::anyhow!("Odd branch order must have 6 elements"));
        }
        if schema.even_branch_order.len() != 8 {
            return Err(anyhow::anyhow!("Even branch order must have 8 elements"));
        }
        let control_block = Arc::new(control_block);
        let programs = Vec::with_capacity(0x80);
        let mut obj = Self {
            base,
            mask: schema.mask,
            offset: schema.offset,
            prolog_order: schema.prolog_order.bytes.clone(),
            odd_branch_order: schema.odd_branch_order.bytes.clone(),
            even_branch_order: schema.even_branch_order.bytes.clone(),
            control_block: control_block,
            programs,
            program_builder,
        };
        for seed in 0..0x80 {
            obj.programs.push(obj.generate_program(seed)?);
        }
        Ok(obj)
    }

    fn new_program(&self, seed: u32) -> Box<dyn ICxProgram + Send + Sync> {
        self.program_builder
            .build(seed, Arc::downgrade(&self.control_block))
    }

    fn generate_program(&self, seed: u32) -> Result<Box<dyn ICxProgram + Send + Sync>> {
        let mut program = self.new_program(seed);
        for stage in (1..=5).rev() {
            if self.emit_code(&mut program, stage) {
                return Ok(program);
            }
            program.clear();
        }
        Err(anyhow::anyhow!("Overly large CxEncryption bytecode"))
    }

    fn read_tpm(tpm_path: &str, filename: &str) -> Result<Vec<u32>> {
        let pfile = Self::get_tpm_path(tpm_path, filename)?;
        let tpm = std::fs::read(&pfile)?;
        let mut begin = 0;
        let end = (tpm.len() - 0x1000) & !0x3;
        while begin < end {
            if &tpm[begin..begin + S_CTL_BLOCK_SIGNATURE.len()] == S_CTL_BLOCK_SIGNATURE {
                let mut control_block = Vec::with_capacity(0x400);
                let mut reader = MemReaderRef::new(&tpm[begin..]);
                for _ in 0..0x400 {
                    control_block.push(!reader.read_u32()?);
                }
                return Ok(control_block);
            }
            begin += 4;
        }
        Err(anyhow::anyhow!(
            "Control block signature not found in TPM file: {}",
            pfile.display()
        ))
    }

    fn get_tpm_path(tpm_path: &str, filename: &str) -> Result<PathBuf> {
        let pb = PathBuf::from(filename);
        let pdir = pb
            .parent()
            .ok_or_else(|| anyhow::anyhow!("Invalid TPM path"))?;
        let pfile = pdir.join(tpm_path);
        if pfile.is_file() {
            return Ok(pfile);
        }
        let pfile = pdir.join("..").join(tpm_path);
        if pfile.is_file() {
            return Ok(pfile);
        }
        Err(anyhow::anyhow!("TPM file not found: {}", tpm_path))
    }

    fn emit_code(&self, program: &mut Box<dyn ICxProgram + Send + Sync>, stage: i32) -> bool {
        program.emit_nop(5)
            && program.emit(MovEdiArg, 4)
            && self.emit_body(program, stage)
            && program.emit_nop(5)
            && program.emit(Retn, 1)
    }

    fn emit_body(&self, program: &mut Box<dyn ICxProgram + Send + Sync>, stage: i32) -> bool {
        if stage == 1 {
            return self.emit_prolog(program);
        }
        if !program.emit(PushEbx, 1) {
            return false;
        }
        if (program.get_random() & 1) != 0 {
            if !self.emit_body(program, stage - 1) {
                return false;
            }
        } else {
            if !self.emit_body2(program, stage - 1) {
                return false;
            }
        }
        if !program.emit(MovEbxEax, 2) {
            return false;
        }
        if (program.get_random() & 1) != 0 {
            if !self.emit_body(program, stage - 1) {
                return false;
            }
        } else {
            if !self.emit_body2(program, stage - 1) {
                return false;
            }
        }
        self.emit_odd_branch(program) && program.emit(PopEbx, 1)
    }

    fn emit_body2(&self, program: &mut Box<dyn ICxProgram + Send + Sync>, stage: i32) -> bool {
        if stage == 1 {
            return self.emit_prolog(program);
        }
        let r = if (program.get_random() & 1) != 0 {
            self.emit_body(program, stage - 1)
        } else {
            self.emit_body2(program, stage - 1)
        };
        r && self.emit_even_branch(program)
    }
    fn emit_prolog(&self, program: &mut Box<dyn ICxProgram + Send + Sync>) -> bool {
        match self.prolog_order[(program.get_random() % 3) as usize] {
            2 => {
                program.emit_nop(5)
                    && program.emit(MovEaxImmed, 2)
                    && {
                        let random = program.get_random() & 0x3ff;
                        program.emit_u32(random)
                    }
                    && program.emit(MovEaxIndirect, 0)
            }
            1 => program.emit(MovEaxEdi, 2),
            0 => program.emit(MovEaxImmed, 1) && program.emit_random(),
            _ => true,
        }
    }

    fn emit_even_branch(&self, program: &mut Box<dyn ICxProgram + Send + Sync>) -> bool {
        match self.even_branch_order[(program.get_random() & 7) as usize] {
            0 => program.emit(NotEax, 2),
            1 => program.emit(DecEax, 1),
            2 => program.emit(NegEax, 2),
            3 => program.emit(IncEax, 1),
            4 => {
                program.emit_nop(5)
                    && program.emit(AndEaxImmed, 1)
                    && program.emit_u32(0x3ff)
                    && program.emit(MovEaxIndirect, 3)
            }
            5 => {
                program.emit(PushEbx, 1)
                    && program.emit(MovEbxEax, 2)
                    && program.emit(AndEbxImmed, 2)
                    && program.emit_u32(0xaaaaaaaa)
                    && program.emit(AndEaxImmed, 1)
                    && program.emit_u32(0x55555555)
                    && program.emit(ShrEbx1, 2)
                    && program.emit(ShlEax1, 2)
                    && program.emit(OrEaxEbx, 2)
                    && program.emit(PopEbx, 1)
            }
            6 => program.emit(XorEaxImmed, 1) && program.emit_random(),
            7 => {
                let mut r = if (program.get_random() & 1) != 0 {
                    program.emit(AddEaxImmed, 1)
                } else {
                    program.emit(SubEaxImmed, 1)
                };
                r = r && program.emit_random();
                r
            }
            _ => true,
        }
    }

    fn emit_odd_branch(&self, program: &mut Box<dyn ICxProgram + Send + Sync>) -> bool {
        match self.odd_branch_order[(program.get_random() % 6) as usize] {
            0 => {
                program.emit(PushEcx, 1)
                    && program.emit(MovEcxEbx, 2)
                    && program.emit(AndEcx0F, 3)
                    && program.emit(ShrEaxCl, 2)
                    && program.emit(PopEcx, 1)
            }
            1 => {
                program.emit(PushEcx, 1)
                    && program.emit(MovEcxEbx, 2)
                    && program.emit(AndEcx0F, 3)
                    && program.emit(ShlEaxCl, 2)
                    && program.emit(PopEcx, 1)
            }
            2 => program.emit(AddEaxEbx, 2),
            3 => program.emit(NegEax, 2) && program.emit(AddEaxEbx, 2),
            4 => program.emit(ImulEaxEbx, 3),
            5 => program.emit(SubEaxEbx, 2),
            _ => true,
        }
    }

    fn execute_xcode(&self, mut hash: u32) -> Result<(u32, u32)> {
        let seed = hash & 0x7f;
        hash >>= 7;
        let program = &self.programs[seed as usize];
        let ret1 = program.execute(hash)?;
        let ret2 = program.execute(!hash)?;
        Ok((ret1, ret2))
    }
}

impl AsRef<BaseSchema> for CxEncryption {
    fn as_ref(&self) -> &BaseSchema {
        &self.base
    }
}

impl ICxEncryption for CxEncryption {
    fn get_base_offset(&self, hash: u32) -> u32 {
        (hash & self.mask).wrapping_add(self.offset)
    }

    fn decode(
        &self,
        key: u32,
        offset: u64,
        buffer: &mut [u8],
        pos: usize,
        count: usize,
    ) -> Result<()> {
        let ret = self.execute_xcode(key)?;
        let key1 = ret.1 >> 16;
        let mut key2 = ret.1 & 0xffff;
        let mut key3 = (ret.0 & 0xFF) as u8;
        if key1 == key2 {
            key2 = key2.wrapping_add(1);
        }
        if key3 == 0 {
            key3 = 1;
        }
        if (key2 as u64) >= offset && (key2 as u64) < offset + (count as u64) {
            buffer[pos + key2 as usize - offset as usize] ^= ((ret.0 >> 16) & 0xFF) as u8;
        }
        if (key1 as u64) >= offset && (key1 as u64) < offset + (count as u64) {
            buffer[pos + key1 as usize - offset as usize] ^= ((ret.0 >> 8) & 0xFF) as u8;
        }
        for i in 0..count {
            buffer[pos + i] ^= key3;
        }
        Ok(())
    }
}

macro_rules! icx_enc_arc_impl {
    ($t:ident) => {
        impl ICxEncryption for Arc<$t> {
            fn get_base_offset(&self, hash: u32) -> u32 {
                self.as_ref().get_base_offset(hash)
            }
            fn inner_decrypt(
                &self,
                key: u32,
                offset: u64,
                buffer: &mut [u8],
                pos: usize,
                count: usize,
            ) -> Result<()> {
                self.as_ref().inner_decrypt(key, offset, buffer, pos, count)
            }
            fn decode(
                &self,
                key: u32,
                offset: u64,
                buffer: &mut [u8],
                pos: usize,
                count: usize,
            ) -> Result<()> {
                self.as_ref().decode(key, offset, buffer, pos, count)
            }
        }
    };
}

macro_rules! icx_enc_impl {
    ($t:ident) => {
        impl ICxEncryption for $t {
            fn get_base_offset(&self, hash: u32) -> u32 {
                self.base.get_base_offset(hash)
            }
            fn inner_decrypt(
                &self,
                key: u32,
                offset: u64,
                buffer: &mut [u8],
                pos: usize,
                count: usize,
            ) -> Result<()> {
                self.base.inner_decrypt(key, offset, buffer, pos, count)
            }
            fn decode(
                &self,
                key: u32,
                offset: u64,
                buffer: &mut [u8],
                pos: usize,
                count: usize,
            ) -> Result<()> {
                self.base.decode(key, offset, buffer, pos, count)
            }
        }
    };
}

icx_enc_arc_impl!(CxEncryption);

impl Crypt for Arc<CxEncryption> {
    base_schema_impl!();
    fn decrypt_supported(&self) -> bool {
        true
    }
    fn decrypt_seek_supported(&self) -> bool {
        true
    }
    fn decrypt<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn Read + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadDebug + Send + Sync + 'a>> {
        let key = (
            entry.file_hash,
            Box::new(self.clone()) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
    fn decrypt_with_seek<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn ReadSeek + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
        let key = (
            entry.file_hash,
            Box::new(self.clone()) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
}

const CX_PROGRAM_SIZE: usize = 0x80;

#[derive(Debug)]
struct CxProgram {
    code: Vec<u32>,
    control_block: Weak<Vec<u32>>,
    length: usize,
    seed: u32,
}

#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, int_enum::IntEnum)]
enum CxByteCode {
    Nop,
    Retn,
    MovEdiArg,
    PushEbx,
    PopEbx,
    PushEcx,
    PopEcx,
    MovEaxEbx,
    MovEbxEax,
    MovEcxEbx,
    MovEaxControlBlock,
    MovEaxEdi,
    MovEaxIndirect,
    AddEaxEbx,
    SubEaxEbx,
    ImulEaxEbx,
    AndEcx0F,
    ShrEbx1,
    ShlEax1,
    ShrEaxCl,
    ShlEaxCl,
    OrEaxEbx,
    NotEax,
    NegEax,
    DecEax,
    IncEax,
    Immed = 0x100,
    MovEaxImmed,
    AndEbxImmed,
    AndEaxImmed,
    XorEaxImmed,
    AddEaxImmed,
    SubEaxImmed,
}

use CxByteCode::*;

#[derive(Debug, Default)]
struct Context {
    eax: u32,
    ebx: u32,
    ecx: u32,
    edi: u32,
    stack: Vec<u32>,
}

#[derive(Debug)]
struct CxProgramBuilder {}

impl Default for CxProgramBuilder {
    fn default() -> Self {
        Self {}
    }
}

trait ICxProgramBuilder: std::fmt::Debug {
    fn build(&self, seed: u32, control_blocks: Weak<Vec<u32>>)
    -> Box<dyn ICxProgram + Send + Sync>;
}

impl ICxProgramBuilder for CxProgramBuilder {
    fn build(
        &self,
        seed: u32,
        control_blocks: Weak<Vec<u32>>,
    ) -> Box<dyn ICxProgram + Send + Sync> {
        Box::new(CxProgram {
            code: Vec::with_capacity(CX_PROGRAM_SIZE),
            control_block: control_blocks,
            length: 0,
            seed,
        })
    }
}

trait ICxProgram: std::fmt::Debug {
    fn execute(&self, hash: u32) -> Result<u32>;
    fn clear(&mut self);
    fn emit_nop(&mut self, count: usize) -> bool;
    fn emit(&mut self, bytecode: CxByteCode, length: usize) -> bool;
    fn emit_u32(&mut self, x: u32) -> bool;
    fn emit_random(&mut self) -> bool {
        let random = self.get_random();
        self.emit_u32(random)
    }
    fn get_random(&mut self) -> u32;
}

impl ICxProgram for CxProgram {
    fn execute(&self, hash: u32) -> Result<u32> {
        let mut context = Context::default();
        let mut iterator = self.code.iter();
        let mut immed = 0u32;
        while let Some(code) = iterator.next() {
            let code = *code;
            const IMMED: u32 = Immed as u32;
            if IMMED == (code & IMMED) {
                immed = *iterator.next().ok_or_else(|| {
                    anyhow::anyhow!("Incomplete IMMED bytecode in CxEncryption program")
                })?;
            }
            let bytecode = CxByteCode::try_from(code).map_err(|_| {
                anyhow::anyhow!("Invalid bytecode in CxEncryption program: {:#X}", code)
            })?;
            match bytecode {
                Nop => {}
                Immed => {}
                MovEdiArg => {
                    context.edi = hash;
                }
                PushEbx => {
                    context.stack.push(context.ebx);
                }
                PopEbx => {
                    context.ebx = context.stack.pop().ok_or_else(|| {
                        anyhow::anyhow!("Stack underflow in CxEncryption program")
                    })?;
                }
                PushEcx => {
                    context.stack.push(context.ecx);
                }
                PopEcx => {
                    context.ecx = context.stack.pop().ok_or_else(|| {
                        anyhow::anyhow!("Stack underflow in CxEncryption program")
                    })?;
                }
                MovEbxEax => {
                    context.ebx = context.eax;
                }
                MovEaxEdi => {
                    context.eax = context.edi;
                }
                MovEcxEbx => {
                    context.ecx = context.ebx;
                }
                MovEaxEbx => {
                    context.eax = context.ebx;
                }
                AndEcx0F => {
                    context.ecx &= 0x0F;
                }
                ShrEbx1 => {
                    context.ebx >>= 1;
                }
                ShlEax1 => {
                    context.eax <<= 1;
                }
                ShrEaxCl => {
                    context.eax >>= context.ecx;
                }
                ShlEaxCl => {
                    context.eax <<= context.ecx;
                }
                OrEaxEbx => {
                    context.eax |= context.ebx;
                }
                NotEax => {
                    context.eax = !context.eax;
                }
                NegEax => {
                    context.eax = context.eax.wrapping_neg();
                }
                DecEax => {
                    context.eax = context.eax.wrapping_sub(1);
                }
                IncEax => {
                    context.eax = context.eax.wrapping_add(1);
                }
                AddEaxEbx => {
                    context.eax = context.eax.wrapping_add(context.ebx);
                }
                SubEaxEbx => {
                    context.eax = context.eax.wrapping_sub(context.ebx);
                }
                ImulEaxEbx => {
                    context.eax = context.eax.wrapping_mul(context.ebx);
                }
                AddEaxImmed => {
                    context.eax = context.eax.wrapping_add(immed);
                }
                SubEaxImmed => {
                    context.eax = context.eax.wrapping_sub(immed);
                }
                AndEbxImmed => {
                    context.ebx &= immed;
                }
                AndEaxImmed => {
                    context.eax &= immed;
                }
                XorEaxImmed => {
                    context.eax ^= immed;
                }
                MovEaxImmed => {
                    context.eax = immed;
                }
                MovEaxIndirect => {
                    let control_block = self
                        .control_block
                        .upgrade()
                        .ok_or_else(|| anyhow::anyhow!("Control block has been dropped"))?;
                    if context.eax as usize >= control_block.len() {
                        return Err(anyhow::anyhow!(
                            "Control block index out of bounds in CxEncryption program: {}",
                            context.eax
                        ));
                    }
                    context.eax = !control_block[context.eax as usize];
                }
                Retn => {
                    if context.stack.len() != 0 {
                        return Err(anyhow::anyhow!(
                            "Stack not empty at RETN in CxEncryption program"
                        ));
                    }
                    return Ok(context.eax);
                }
                _ => {
                    return Err(anyhow::anyhow!(
                        "Unsupported bytecode in CxEncryption program: {:?}",
                        bytecode
                    ));
                }
            }
        }
        Err(anyhow::anyhow!(
            "CxEncryption program without RETN bytecode"
        ))
    }

    fn clear(&mut self) {
        self.length = 0;
        self.code.clear();
    }

    fn emit_nop(&mut self, count: usize) -> bool {
        if self.length + count > CX_PROGRAM_SIZE {
            return false;
        }
        self.length += count;
        return true;
    }

    fn emit(&mut self, bytecode: CxByteCode, length: usize) -> bool {
        if self.length + length > CX_PROGRAM_SIZE {
            return false;
        }
        self.code.push(bytecode as u32);
        self.length += length;
        return true;
    }

    fn emit_u32(&mut self, x: u32) -> bool {
        if self.length + 4 > CX_PROGRAM_SIZE {
            return false;
        }
        self.code.push(x);
        self.length += 4;
        return true;
    }

    fn get_random(&mut self) -> u32 {
        let seed = self.seed;
        self.seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
        self.seed ^ (seed << 16) ^ (seed >> 16)
    }
}

#[derive(msg_tool_macro::MyDebug)]
struct CxEncryptionReader<'a, T> {
    #[skip_fmt]
    inner: T,
    seg_start: u64,
    seg_size: u64,
    pos: u64,
    key: (u32, Box<dyn ICxEncryption + Send + Sync + 'a>),
}

impl<'a, T: Read> CxEncryptionReader<'a, T> {
    pub fn new(
        inner: T,
        seg: &Segment,
        key: (u32, Box<dyn ICxEncryption + Send + Sync + 'a>),
    ) -> Self {
        Self {
            inner,
            seg_start: seg.offset_in_file,
            seg_size: seg.original_size,
            pos: 0,
            key,
        }
    }
}

impl<'a, T: Read + Seek> Seek for CxEncryptionReader<'a, T> {
    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
        let new_pos: i64 = match pos {
            SeekFrom::Start(offset) => offset as i64,
            SeekFrom::End(offset) => self.seg_size as i64 + offset,
            SeekFrom::Current(offset) => self.pos as i64 + offset,
        };
        let offset = new_pos - self.pos as i64;
        if offset != 0 {
            self.inner.seek(SeekFrom::Current(offset))?;
            self.pos = new_pos as u64;
        }
        Ok(self.pos)
    }
}

impl<'a, R: Read> Read for CxEncryptionReader<'a, R> {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        let offset = self.seg_start + self.pos;
        let count = self.inner.read(buf)?;
        if count == 0 {
            return Ok(0);
        }
        let key = self.key.0;
        let cx = &self.key.1;
        if let Err(e) = cx.inner_decrypt(key, offset, buf, 0, count) {
            return Err(std::io::Error::new(std::io::ErrorKind::Other, e));
        }
        self.pos += count as u64;
        Ok(count)
    }
}

#[derive(Debug)]
pub struct SenrenCxCrypt {
    base: CxEncryption,
    names_section_id: String,
}

impl AsRef<BaseSchema> for SenrenCxCrypt {
    fn as_ref(&self) -> &BaseSchema {
        self.base.as_ref()
    }
}

impl SenrenCxCrypt {
    pub fn new(
        base: BaseSchema,
        schema: &CxSchema,
        filename: &str,
        names_section_id: String,
    ) -> Result<Arc<Self>> {
        Ok(Arc::new(Self::new_inner(
            base,
            schema,
            filename,
            Box::new(CxProgramBuilder::default()),
            names_section_id,
        )?))
    }
    fn new_inner(
        base: BaseSchema,
        schema: &CxSchema,
        filename: &str,
        program_builder: Box<dyn ICxProgramBuilder + Send + Sync>,
        names_section_id: String,
    ) -> Result<Self> {
        let cx = CxEncryption::new_inner(base, schema, filename, program_builder)?;
        Ok(Self {
            base: cx,
            names_section_id,
        })
    }

    fn read_yuzu_names<'a>(
        reader: Box<dyn ReadDebug + 'a>,
        unpacked_size: u32,
    ) -> Result<(HashMap<u32, String>, HashMap<String, String>)> {
        let mut decoded = MemWriter::with_capacity(unpacked_size as usize);
        {
            let mut decoder = flate2::read::ZlibDecoder::new(reader);
            std::io::copy(&mut decoder, &mut decoded)?;
        }
        let decoded = decoded.into_inner();
        let mut reader = MemReader::new(decoded);
        let mut hash_map = HashMap::new();
        let mut md5_map = HashMap::new();
        let mut dir_offset = 0u64;
        while !reader.is_eof() {
            let _entry_sign = reader.read_u32()?;
            let mut entry_size = reader.read_u64()?;
            dir_offset += 12 + entry_size;
            let hash = reader.read_u32()?;
            let name_len = reader.read_u16()?;
            entry_size -= 6;
            if (name_len as u64) * 2 <= entry_size {
                let name = reader.read_exact_vec((name_len) as usize * 2)?;
                let name = decode_to_string(Encoding::Utf16LE, &name, true)?;
                if !hash_map.contains_key(&hash) {
                    hash_map.insert(hash, name.clone());
                }
                let encoded = encode_string(Encoding::Utf16LE, &name.to_ascii_lowercase(), true)?;
                let md5 = format!("{:x}", md5::compute(encoded));
                md5_map.insert(md5, name);
            }
            reader.pos = dir_offset as usize;
            md5_map.insert("$".into(), "startup.tjs".into());
        }
        Ok((hash_map, md5_map))
    }
}

fn read_yuzu_names<'a, T>(
    archive: &mut Xp3Archive<'a>,
    names_section_id: &str,
    convert: T,
) -> Result<()>
where
    T: FnOnce(
        Box<dyn ReadDebug + 'a>,
        u32,
    ) -> Result<(HashMap<u32, String>, HashMap<String, String>)>,
{
    if let Some(section) = archive.extras.iter().find(|s| s.tag == names_section_id) {
        let mut sreader = MemReaderRef::new(&section.data);
        let offset = sreader.read_u64()? + archive.base_offset;
        let unpacked_size = sreader.read_u32()?;
        let packed_size = sreader.read_u32()?;
        let index_stream =
            MutexWrapper::new(archive.inner.clone(), offset).take(packed_size as u64);
        let (hash_map, md5_map) = convert(Box::new(index_stream), unpacked_size)?;
        for entry in archive.entries.iter_mut() {
            if let Some(name) = hash_map.get(&entry.file_hash) {
                entry.name = name.clone();
            } else if let Some(name) = md5_map.get(&entry.name) {
                entry.name = name.clone();
            }
        }
    }
    archive.extras.retain(|s| s.tag != names_section_id);
    Ok(())
}

icx_enc_impl!(SenrenCxCrypt);
icx_enc_arc_impl!(SenrenCxCrypt);

impl Crypt for Arc<SenrenCxCrypt> {
    base_schema_impl!();
    fn init(&self, archive: &mut Xp3Archive) -> Result<()> {
        default_init_crypt(archive)?;
        read_yuzu_names(
            archive,
            &self.names_section_id,
            SenrenCxCrypt::read_yuzu_names,
        )
    }
    fn decrypt_supported(&self) -> bool {
        true
    }
    fn decrypt_seek_supported(&self) -> bool {
        true
    }
    fn decrypt<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn Read + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadDebug + Send + Sync + 'a>> {
        let key = (
            entry.file_hash,
            Box::new(self.clone()) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
    fn decrypt_with_seek<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn ReadSeek + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
        let key = (
            entry.file_hash,
            Box::new(self.clone()) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
}

#[derive(Debug)]
struct CxProgramNana {
    base: CxProgram,
    random_seed: u32,
}

impl CxProgramNana {
    fn new(seed: u32, control_blocks: Weak<Vec<u32>>, random_seed: u32) -> Self {
        Self {
            base: CxProgram {
                code: Vec::with_capacity(CX_PROGRAM_SIZE),
                control_block: control_blocks,
                length: 0,
                seed,
            },
            random_seed,
        }
    }
}

impl ICxProgram for CxProgramNana {
    fn execute(&self, hash: u32) -> Result<u32> {
        self.base.execute(hash)
    }
    fn clear(&mut self) {
        self.base.clear();
    }
    fn emit(&mut self, bytecode: CxByteCode, length: usize) -> bool {
        self.base.emit(bytecode, length)
    }
    fn emit_nop(&mut self, count: usize) -> bool {
        self.base.emit_nop(count)
    }
    fn emit_u32(&mut self, x: u32) -> bool {
        self.base.emit_u32(x)
    }
    fn get_random(&mut self) -> u32 {
        let mut s = self.base.seed ^ (self.base.seed << 17);
        s ^= (s << 18) | (s >> 15);
        self.base.seed = !s;
        let mut r = self.random_seed ^ (self.random_seed << 13);
        r ^= r >> 17;
        self.random_seed = r ^ (r << 5);
        self.base.seed ^ self.random_seed
    }
}

#[derive(Debug)]
struct CxProgramNanaBuilder {
    random_seed: u32,
}

impl CxProgramNanaBuilder {
    fn new(random_seed: u32) -> Self {
        Self { random_seed }
    }
}

impl ICxProgramBuilder for CxProgramNanaBuilder {
    fn build(
        &self,
        seed: u32,
        control_blocks: Weak<Vec<u32>>,
    ) -> Box<dyn ICxProgram + Send + Sync> {
        Box::new(CxProgramNana::new(seed, control_blocks, self.random_seed))
    }
}

#[derive(Debug)]
pub struct CabbageCxCrypt {
    base: SenrenCxCrypt,
}

impl AsRef<BaseSchema> for CabbageCxCrypt {
    fn as_ref(&self) -> &BaseSchema {
        self.base.as_ref()
    }
}

impl CabbageCxCrypt {
    pub fn new(
        base: BaseSchema,
        schema: &CxSchema,
        filename: &str,
        names_section_id: String,
        random_seed: u32,
    ) -> Result<Arc<Self>> {
        Ok(Arc::new(Self {
            base: SenrenCxCrypt::new_inner(
                base,
                schema,
                filename,
                Box::new(CxProgramNanaBuilder::new(random_seed)),
                names_section_id,
            )?,
        }))
    }
}

icx_enc_impl!(CabbageCxCrypt);
icx_enc_arc_impl!(CabbageCxCrypt);

impl Crypt for Arc<CabbageCxCrypt> {
    base_schema_impl!();
    fn init(&self, archive: &mut Xp3Archive) -> Result<()> {
        default_init_crypt(archive)?;
        read_yuzu_names(
            archive,
            &self.base.names_section_id,
            SenrenCxCrypt::read_yuzu_names,
        )
    }
    fn decrypt_supported(&self) -> bool {
        true
    }
    fn decrypt_seek_supported(&self) -> bool {
        true
    }
    fn decrypt<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn Read + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadDebug + Send + Sync + 'a>> {
        let key = (
            entry.file_hash,
            Box::new(self.clone()) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
    fn decrypt_with_seek<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn ReadSeek + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
        let key = (
            entry.file_hash,
            Box::new(self.clone()) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
}

#[derive(Debug)]
struct NanaDecryptor {
    state: [u32; 27],
    seed: u64,
}

impl NanaDecryptor {
    fn new(key: &[u32], seed1: u32, seed2: u32) -> Self {
        let mut state = [0u32; 27];
        let seed = (seed2 as u64) << 32 | (seed1 as u64);
        let mut s = [0u32; 3];
        let mut k = key[0];
        s[0] = key[1];
        s[1] = key[2];
        s[2] = key[3];
        state[0] = k;
        let mut dst = 1;
        for i in 0..26usize {
            let src = i % 3;
            let m = s[src].rotate_right(8);
            let n = (i as u32) ^ k.wrapping_add(m);
            k = n ^ k.rotate_left(3);
            state[dst] = k;
            dst += 1;
            s[src] = n;
        }
        Self { state, seed }
    }

    fn decrypt(&self, data: &mut [u8]) {
        let mut i = 0;
        let mut offset = 0;
        let mut length = data.len();
        while length > 0 {
            offset += 1;
            let mut key = self.transform_key(offset ^ self.seed);
            let count = std::cmp::min(length, 8);
            for _ in 0..count {
                data[i] ^= (key & 0xFF) as u8;
                key >>= 8;
                i += 1;
            }
            length -= count;
        }
    }

    fn transform_key(&self, key: u64) -> u64 {
        let mut lo = (key & 0xFFFFFFFF) as u32;
        let mut hi = (key >> 32) as u32;
        for i in 0..27 {
            hi = hi.rotate_right(8);
            hi = hi.wrapping_add(lo);
            hi ^= self.state[i];
            lo = lo.rotate_left(3);
            lo ^= hi;
        }
        (hi as u64) << 32 | (lo as u64)
    }
}

#[derive(Debug)]
pub struct NanaCxCrypt {
    base: SenrenCxCrypt,
    decryptor: NanaDecryptor,
}

impl AsRef<BaseSchema> for NanaCxCrypt {
    fn as_ref(&self) -> &BaseSchema {
        self.base.as_ref()
    }
}

impl NanaCxCrypt {
    pub fn new(
        base: BaseSchema,
        schema: &CxSchema,
        filename: &str,
        names_section_id: String,
        random_seed: u32,
        yuz_key: &[u32],
    ) -> Result<Arc<Self>> {
        if yuz_key.len() != 6 {
            return Err(anyhow::anyhow!(
                "Invalid Yuzu keys for NanaCxCrypt: expected 6, got {}",
                yuz_key.len()
            ));
        }
        let cx = SenrenCxCrypt::new_inner(
            base,
            schema,
            filename,
            Box::new(CxProgramNanaBuilder::new(random_seed)),
            names_section_id,
        )?;
        let decryptor = NanaDecryptor::new(yuz_key, yuz_key[4], yuz_key[5]);
        Ok(Arc::new(Self {
            base: cx,
            decryptor,
        }))
    }

    fn read_yuzu_names<'a>(
        &self,
        mut reader: Box<dyn ReadDebug + 'a>,
        unpacked_size: u32,
    ) -> Result<(HashMap<u32, String>, HashMap<String, String>)> {
        let mut prefix = Vec::with_capacity(0x100);
        (&mut reader).take(0x100).read_to_end(&mut prefix)?;
        self.decryptor.decrypt(&mut prefix);
        let reader = Box::new(PrefixStream::new(prefix, reader));
        SenrenCxCrypt::read_yuzu_names(reader, unpacked_size)
    }
}

icx_enc_impl!(NanaCxCrypt);
icx_enc_arc_impl!(NanaCxCrypt);

impl Crypt for Arc<NanaCxCrypt> {
    base_schema_impl!();
    fn init(&self, archive: &mut Xp3Archive) -> Result<()> {
        default_init_crypt(archive)?;
        read_yuzu_names(
            archive,
            &self.base.names_section_id,
            |reader, unpacked_size| self.read_yuzu_names(reader, unpacked_size),
        )
    }
    fn decrypt_supported(&self) -> bool {
        true
    }
    fn decrypt_seek_supported(&self) -> bool {
        true
    }
    fn decrypt<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn Read + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadDebug + Send + Sync + 'a>> {
        let key = (
            entry.file_hash,
            Box::new(self.clone()) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
    fn decrypt_with_seek<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn ReadSeek + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
        let key = (
            entry.file_hash,
            Box::new(self.clone()) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
}

#[derive(Debug)]
struct YuzDecryptor {
    state: [u8; 64],
}

impl YuzDecryptor {
    fn new(key1: &[u32], key2: &[u32], seed1: u32, seed2: u32) -> Self {
        let mut state = [0u8; 64];
        for i in 0..4 {
            state[i * 4..i * 4 + 4].copy_from_slice(&key2[i].to_le_bytes());
        }
        for i in 0..8 {
            state[i * 4 + 16..i * 4 + 20].copy_from_slice(&key1[i].to_le_bytes());
        }
        let t: u32 = !0;
        state[48..52].copy_from_slice(&t.to_le_bytes());
        state[52..56].copy_from_slice(&t.to_le_bytes());
        state[56..60].copy_from_slice(&(!seed1).to_le_bytes());
        state[60..64].copy_from_slice(&(!seed2).to_le_bytes());
        Self { state }
    }

    fn decrypt(&self, data: &mut [u8]) {
        let mut state1 = [0u8; 64];
        let mut state2 = [0u8; 64];
        let mut i = 0;
        let mut offset: u64 = 0;
        let mut length = data.len();
        while length > 0 {
            state1.copy_from_slice(&self.state);
            state1[48..56].copy_from_slice(&(!offset).to_le_bytes());
            offset += 1;
            Self::transform_state(&state1, &mut state2, 8);
            let count = length.min(0x40);
            for j in 0..count {
                data[i] ^= state2[j];
                i += 1;
            }
            length -= count;
        }
    }

    fn transform_state(state: &[u8], target: &mut [u8], length: usize) {
        let mut tmp = [0u32; 16];
        for i in 0..16 {
            tmp[i] = !u32::from_le_bytes([
                state[i * 4],
                state[i * 4 + 1],
                state[i * 4 + 2],
                state[i * 4 + 3],
            ]);
        }
        if length > 0 {
            for _ in 0..((length - 1) >> 1) + 1 {
                let mut t1 = w!(tmp[4] + tmp[0]);
                let mut t2 = (t1 ^ tmp[12]).rotate_left(16);
                let mut t3 = w!(t2 + tmp[8]);
                let mut t4 = (tmp[4] ^ t3).rotate_left(12);
                let mut t5 = w!(t4 + t1);
                let mut t6 = (t5 ^ t2).rotate_left(8);
                tmp[12] = t6;
                w!(t6 += t3);
                tmp[4] = (t4 ^ t6).rotate_left(7);
                t4 = (w!(tmp[5] + tmp[1]) ^ tmp[13]).rotate_left(16);
                t3 = (tmp[5] ^ w!(t4 + tmp[9])).rotate_left(12);
                t2 = w!(t3 + tmp[5] + tmp[1]);
                tmp[13] = (t2 ^ t4).rotate_left(8);
                w!(tmp[9] += tmp[13] + t4);
                tmp[5] = (t3 ^ tmp[9]).rotate_left(7);
                t4 = (w!(tmp[6] + tmp[2]) ^ tmp[14]).rotate_left(16);
                w!(tmp[10] += t4);
                t1 = (tmp[6] ^ tmp[10]).rotate_left(12);
                t3 = w!(t1 + tmp[6] + tmp[2]);
                tmp[14] = (t3 ^ t4).rotate_left(8);
                tmp[6] = (t1 ^ w!(tmp[14] + tmp[10])).rotate_left(7);
                w!(tmp[10] += tmp[14]);
                t4 = w!(tmp[7] + tmp[3]) ^ tmp[15];
                w!(tmp[3] += tmp[7]);
                t4 = t4.rotate_left(16);
                w!(tmp[11] += t4);
                t1 = (tmp[7] ^ tmp[11]).rotate_left(12);
                t4 ^= w!(t1 + tmp[3]);
                w!(tmp[3] += t1);
                t4 = t4.rotate_left(8);
                w!(tmp[11] += t4);
                t1 = (t1 ^ tmp[11]).rotate_left(7);
                w!(t5 += tmp[5]);
                w!(t2 += tmp[6]);
                t4 = (t5 ^ t4).rotate_left(16);
                w!(tmp[10] += t4);
                tmp[5] = (tmp[5] ^ tmp[10]).rotate_left(12);
                tmp[0] = w!(tmp[5] + t5);
                t4 = (tmp[0] ^ t4).rotate_left(8);
                tmp[15] = t4;
                w!(tmp[10] += t4);
                tmp[5] = (tmp[5] ^ tmp[10]).rotate_left(7);
                tmp[12] = (tmp[12] ^ t2).rotate_left(16);
                w!(tmp[11] += tmp[12]);
                t4 = (tmp[11] ^ tmp[6]).rotate_left(12);
                tmp[1] = w!(t4 + t2);
                tmp[12] = (tmp[12] ^ tmp[1]).rotate_left(8);
                w!(tmp[11] += tmp[12]);
                tmp[6] = (t4 ^ tmp[11]).rotate_left(7);
                w!(t3 += t1);
                t4 = (tmp[13] ^ t3).rotate_left(16);
                t2 = w!(t4 + t6);
                t1 = (t2 ^ t1).rotate_left(12);
                tmp[2] = w!(t1 + t3);
                tmp[13] = (t4 ^ tmp[2]).rotate_left(8);
                tmp[8] = w!(tmp[13] + t2);
                tmp[7] = (tmp[8] ^ t1).rotate_left(7);
                t6 = (tmp[14] ^ w!(tmp[4] + tmp[3])).rotate_left(16);
                t1 = (tmp[4] ^ w!(t6 + tmp[9])).rotate_left(12);
                w!(tmp[3] += t1 + tmp[4]);
                t3 = (t6 ^ tmp[3]).rotate_left(8);
                w!(tmp[9] += t3 + t6);
                tmp[4] = (t1 ^ tmp[9]).rotate_left(7);
                tmp[14] = t3;
            }
        }
        let mut pos = 0;
        for i in 0..16 {
            let d =
                !u32::from_le_bytes([state[pos], state[pos + 1], state[pos + 2], state[pos + 3]]);
            let d = w!(tmp[i] + d);
            target[pos..pos + 4].copy_from_slice(&d.to_le_bytes());
            pos += 4;
        }
    }
}

#[derive(Debug)]
pub struct RiddleCxCrypt {
    base: SenrenCxCrypt,
    decryptor: YuzDecryptor,
    key1: u32,
    key2: u32,
}

impl AsRef<BaseSchema> for RiddleCxCrypt {
    fn as_ref(&self) -> &BaseSchema {
        self.base.as_ref()
    }
}

impl RiddleCxCrypt {
    pub fn new(
        base: BaseSchema,
        schema: &CxSchema,
        filename: &str,
        names_section_id: String,
        random_seed: u32,
        yuz_key: &[u32],
        key1: u32,
        key2: u32,
    ) -> Result<Arc<Self>> {
        if yuz_key.len() != 6 {
            return Err(anyhow::anyhow!(
                "Invalid Yuzu keys for RiddleCxCrypt: expected 6, got {}",
                yuz_key.len()
            ));
        }
        let cx = SenrenCxCrypt::new_inner(
            base,
            schema,
            filename,
            Box::new(CxProgramNanaBuilder::new(random_seed)),
            names_section_id,
        )?;
        let control_block = cx.base.control_block.as_ref();
        let decryptor = YuzDecryptor::new(&control_block, yuz_key, yuz_key[4], yuz_key[5]);
        Ok(Arc::new(Self {
            base: cx,
            decryptor,
            key1,
            key2,
        }))
    }

    fn get_key_from_hash(&self, key: u32) -> u64 {
        let lo = key ^ self.key2;
        let mut hi = (key << 13) ^ key;
        hi ^= hi >> 17;
        hi ^= (hi << 5) ^ self.key1;
        ((hi as u64) << 32) | (lo as u64)
    }

    fn read_yuzu_names<'a>(
        &self,
        mut reader: Box<dyn ReadDebug + 'a>,
        unpacked_size: u32,
    ) -> Result<(HashMap<u32, String>, HashMap<String, String>)> {
        let mut prefix = Vec::with_capacity(0x100);
        (&mut reader).take(0x100).read_to_end(&mut prefix)?;
        self.decryptor.decrypt(&mut prefix);
        let reader = Box::new(PrefixStream::new(prefix, reader));
        SenrenCxCrypt::read_yuzu_names(reader, unpacked_size)
    }
}

impl ICxEncryption for RiddleCxCrypt {
    fn get_base_offset(&self, hash: u32) -> u32 {
        self.base.get_base_offset(hash)
    }
    fn inner_decrypt(
        &self,
        key: u32,
        offset: u64,
        buffer: &mut [u8],
        pos: usize,
        count: usize,
    ) -> Result<()> {
        if offset < 8 && count > 0 {
            let mut key = self.get_key_from_hash(key);
            key >>= offset << 3;
            let first_chunk = count.min(8 - offset as usize);
            for i in 0..first_chunk {
                buffer[pos + i] ^= (key & 0xFF) as u8;
                key >>= 8;
            }
        }
        self.base.inner_decrypt(key, offset, buffer, pos, count)
    }
    fn decode(
        &self,
        key: u32,
        offset: u64,
        buffer: &mut [u8],
        pos: usize,
        count: usize,
    ) -> Result<()> {
        self.base.decode(key, offset, buffer, pos, count)
    }
}
icx_enc_arc_impl!(RiddleCxCrypt);

impl Crypt for Arc<RiddleCxCrypt> {
    base_schema_impl!();
    fn init(&self, archive: &mut Xp3Archive) -> Result<()> {
        default_init_crypt(archive)?;
        read_yuzu_names(
            archive,
            &self.base.names_section_id,
            |reader, unpacked_size| self.read_yuzu_names(reader, unpacked_size),
        )
    }
    fn decrypt_supported(&self) -> bool {
        true
    }
    fn decrypt_seek_supported(&self) -> bool {
        true
    }
    fn decrypt<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn Read + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadDebug + Send + Sync + 'a>> {
        let key = (
            entry.file_hash,
            Box::new(self.clone()) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
    fn decrypt_with_seek<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn ReadSeek + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
        let key = (
            entry.file_hash,
            Box::new(self.clone()) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
}

#[derive(Debug)]
pub struct HxCryptLite {
    base: CxEncryption,
    header_key: Option<Vec<u8>>,
    header_split_position: u64,
    file_crypt_flag: bool,
}

impl HxCryptLite {
    pub fn new(
        base: BaseSchema,
        schema: &CxSchema,
        filename: &str,
        header_key: Option<Vec<u8>>,
        header_split_position: u64,
        file_crypt_flag: bool,
        random_type: i32,
    ) -> Result<Arc<Self>> {
        if let Some(key) = header_key.as_ref() {
            if key.len() < 8 {
                anyhow::bail!("header_key is too small.");
            }
        }
        Ok(Arc::new(Self {
            base: CxEncryption::new_inner(
                base,
                schema,
                filename,
                Box::new(HxProgramLiteBuilder::new(random_type)),
            )?,
            header_key,
            header_split_position,
            file_crypt_flag,
        }))
    }
}

impl AsRef<BaseSchema> for HxCryptLite {
    fn as_ref(&self) -> &BaseSchema {
        self.base.as_ref()
    }
}

#[derive(Debug)]
struct HxProgramLite {
    base: CxProgram,
    random_type: i32,
    random_block: [u32; 0x270],
    block_position: usize,
}

impl HxProgramLite {
    pub fn new(seed: u32, control_block: Weak<Vec<u32>>, random_method: i32) -> Self {
        let block_position = 0x270;
        let mut block = [0; 0x270];
        block[0] = seed;
        for i in 1..0x270 {
            block[i] =
                ((block[i - 1] ^ (block[i - 1] >> 0x1E)) * 0x6C078965).wrapping_add(i as u32);
        }
        Self {
            base: CxProgram {
                code: Vec::with_capacity(CX_PROGRAM_SIZE),
                control_block,
                length: 0,
                seed,
            },
            random_type: random_method,
            random_block: block,
            block_position,
        }
    }

    fn get_random_new(&mut self) -> u32 {
        if self.block_position == 0x270 {
            self.transform_block();
        }
        let s0 = self.random_block[self.block_position];
        let s1 = (s0 >> 11) ^ s0;
        let s2 = ((s1 & 0xFF3A58AD) << 7) ^ s1;
        let s3 = ((s2 & 0xFFFFDF8C) << 15) ^ s2;
        let s4 = (s3 >> 18) ^ s3;
        self.block_position += 1;
        s4
    }

    fn transform_block(&mut self) {
        self.block_position = 0;
        let block = &mut self.random_block;
        // 0-0xE2
        for i in 0..0xE3 {
            let s0 = if (block[i + 1] & 1) != 0 {
                0x9908B0DFu32
            } else {
                0
            };
            let s1 = (((block[i] ^ block[i + 1]) & 0x7FFFFFFE) ^ block[i]) >> 1;
            let s2 = s0 ^ s1 ^ block[i + 0x18D];
            block[i] = s2;
        }
        // 0xE3-0x26E
        for i in 0..0x18C {
            let s0 = if (block[i + 1 + 0xE3] & 1) != 0 {
                0x9908B0DFu32
            } else {
                0
            };
            let s1 =
                (((block[i + 0xE3] ^ block[i + 1 + 0xE3]) & 0x7FFFFFFE) ^ block[i + 0xE3]) >> 1;
            let s2 = s0 ^ s1 ^ block[i];
            block[i + 0xE3] = s2;
        }
        // 0x26F
        let s0 = if (block[0] & 1) != 0 {
            0x9908B0DFu32
        } else {
            0
        };
        let s1 = (((block[0x26F] ^ block[0]) & 0x7FFFFFFE) ^ block[0x26F]) >> 1;
        let s2 = s0 ^ s1 ^ block[0x18C];
        block[0x26F] = s2;
    }
}

impl ICxProgram for HxProgramLite {
    fn execute(&self, hash: u32) -> Result<u32> {
        self.base.execute(hash)
    }
    fn clear(&mut self) {
        self.base.clear();
    }
    fn emit(&mut self, bytecode: CxByteCode, length: usize) -> bool {
        self.base.emit(bytecode, length)
    }
    fn emit_nop(&mut self, count: usize) -> bool {
        self.base.emit_nop(count)
    }
    fn emit_u32(&mut self, x: u32) -> bool {
        self.base.emit_u32(x)
    }
    fn get_random(&mut self) -> u32 {
        if self.random_type == 0 {
            self.base.get_random()
        } else {
            self.get_random_new()
        }
    }
}

#[derive(Debug)]
struct HxProgramLiteBuilder {
    random_method: i32,
}

impl HxProgramLiteBuilder {
    pub fn new(random_method: i32) -> Self {
        Self { random_method }
    }
}

impl ICxProgramBuilder for HxProgramLiteBuilder {
    fn build(
        &self,
        seed: u32,
        control_blocks: Weak<Vec<u32>>,
    ) -> Box<dyn ICxProgram + Send + Sync> {
        Box::new(HxProgramLite::new(seed, control_blocks, self.random_method))
    }
}

struct HxFileDecryptor {
    split_pos1: u64,
    split_pos2: u64,
    key: u32,
    key1: u8,
    key2: u8,
}

impl HxFileDecryptor {
    fn new(key: u64, file_key_flag: bool) -> Self {
        let key_ptr = key.to_le_bytes();
        let mut global_key = key_ptr[0] as u32;
        let mut key1 = key_ptr[1];
        let mut key2 = key_ptr[2];
        let split_pos1 = u16::from_le_bytes([key_ptr[6], key_ptr[7]]) as u64;
        let mut split_pos2 = u16::from_le_bytes([key_ptr[4], key_ptr[5]]) as u64;
        if split_pos1 == split_pos2 {
            split_pos2 += 1;
        }
        if global_key == 0 {
            global_key = 1;
        }
        global_key = global_key.wrapping_mul(0x01010101);
        if file_key_flag {
            key1 = 0;
            key2 = 0;
        }
        Self {
            split_pos1,
            split_pos2,
            key: global_key,
            key1,
            key2,
        }
    }

    fn decrypt(&self, data: &mut [u8], offset: u64, pos: usize, count: usize) {
        if count == 0 {
            return;
        }
        let key = self.key.to_le_bytes();
        let mut key_pos = (offset & 3) as usize;
        for i in 0..count {
            data[pos + i] ^= key[key_pos];
            key_pos = (key_pos + 1) & 3;
        }
        let count = count as u64;
        if self.split_pos1 >= offset && self.split_pos1 < offset + count {
            data[(self.split_pos1 - offset) as usize + pos] ^= self.key1;
        }
        if self.split_pos2 >= offset && self.split_pos2 < offset + count {
            data[(self.split_pos2 - offset) as usize + pos] ^= self.key2;
        }
    }
}

struct HxHeaderDecryptor {
    key: [u8; 8],
    pos: u64,
}

impl HxHeaderDecryptor {
    fn new(hash: u32, key: &[u8], pos: u64) -> Self {
        let key_ptr = [
            u32::from_le_bytes([key[0], key[1], key[2], key[3]]),
            u32::from_le_bytes([key[4], key[5], key[6], key[7]]),
        ];
        let s0 = hash ^ key_ptr[1];
        let s1 = hash ^ (hash << 13);
        let s2 = s1 ^ (s1 >> 17);
        let s3 = s2 ^ (s2 << 5) ^ key_ptr[0];
        let key = ((s3 as u64) << 32) | (s0 as u64);
        Self {
            key: key.to_le_bytes(),
            pos,
        }
    }

    fn decrypt(&self, data: &mut [u8], offset: u64, pos: usize, count: usize) {
        let mut start_pos = offset;
        if start_pos <= self.pos {
            start_pos = self.pos;
        }
        let mut end_pos = offset + count as u64;
        if end_pos >= self.pos + 8 {
            end_pos = self.pos + 8;
        }
        if start_pos >= end_pos {
            return;
        }
        let dlen = end_pos - start_pos;
        let key_start_index = start_pos - self.pos;
        let data_start_index = start_pos - offset + pos as u64;
        for i in 0..dlen {
            data[(data_start_index + i) as usize] ^= self.key[(key_start_index + i) as usize];
        }
    }
}

impl ICxEncryption for HxCryptLite {
    fn get_base_offset(&self, _hash: u32) -> u32 {
        _hash
    }
    fn inner_decrypt(
        &self,
        hash: u32,
        offset: u64,
        buffer: &mut [u8],
        pos: usize,
        count: usize,
    ) -> Result<()> {
        if let Some(key) = self.header_key.as_ref() {
            let dec = HxHeaderDecryptor::new(hash, &key, self.header_split_position);
            dec.decrypt(buffer, offset, pos, count);
        }
        let ret1 = self.base.execute_xcode(hash)?;
        let ret2 = self.base.execute_xcode(hash ^ (hash >> 16))?;
        let key1 = ((ret1.1 as u64) << 32) | (ret1.0 as u64);
        let key2 = ((ret2.1 as u64) << 32) | (ret2.0 as u64);
        let split_pos = (self.base.offset + (hash & self.base.mask)) as u64;
        let dec1 = HxFileDecryptor::new(key1, self.file_crypt_flag);
        let dec2 = HxFileDecryptor::new(key2, self.file_crypt_flag);
        if split_pos > offset {
            if split_pos < offset + count as u64 {
                let blen1 = split_pos - offset;
                let blen2 = offset + count as u64 - split_pos;
                dec1.decrypt(buffer, offset, pos, blen1 as usize);
                dec2.decrypt(buffer, offset + blen1, pos + blen1 as usize, blen2 as usize);
            } else {
                dec1.decrypt(buffer, offset, pos, count);
            }
        } else {
            dec2.decrypt(buffer, offset, pos, count);
        }
        Ok(())
    }
    fn decode(
        &self,
        _key: u32,
        _offset: u64,
        _buffer: &mut [u8],
        _pos: usize,
        _count: usize,
    ) -> Result<()> {
        Ok(())
    }
}

icx_enc_arc_impl!(HxCryptLite);

impl Crypt for Arc<HxCryptLite> {
    base_schema_impl!();
    fn decrypt_supported(&self) -> bool {
        true
    }
    fn decrypt_seek_supported(&self) -> bool {
        true
    }
    fn decrypt<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn Read + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadDebug + Send + Sync + 'a>> {
        let key = (
            entry.file_hash,
            Box::new(self.clone()) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
    fn decrypt_with_seek<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn ReadSeek + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
        let key = (
            entry.file_hash,
            Box::new(self.clone()) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
}

#[derive(Copy, Clone, PartialEq, Eq, Hash)]
struct FileHash([u8; 32]);

impl std::fmt::Debug for FileHash {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "FileHash(")?;
        write!(f, "{}", hex::encode(self.0))?;
        write!(f, ")")
    }
}

impl std::fmt::Display for FileHash {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", hex::encode(self.0))
    }
}

impl<'a> TryFrom<&'a [u8]> for FileHash {
    type Error = anyhow::Error;
    fn try_from(value: &'a [u8]) -> Result<Self> {
        Ok(Self(value.try_into()?))
    }
}

impl<'a> TryFrom<&'a str> for FileHash {
    type Error = anyhow::Error;
    fn try_from(value: &'a str) -> Result<Self> {
        Self::try_from(hex::decode(value)?.as_slice())
    }
}

impl<'de> Deserialize<'de> for FileHash {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        let bytes = hex::decode(&s).map_err(de::Error::custom)?;
        let arr = bytes.try_into().map_err(|bytes: Vec<u8>| {
            de::Error::custom(format!(
                "FileHash length mismatch: expected 32 bytes, got {}",
                bytes.len()
            ))
        })?;
        Ok(FileHash(arr))
    }
}

impl Serialize for FileHash {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&hex::encode(self.0))
    }
}

impl FileHash {
    fn to_string(&self) -> String {
        hex::encode(&self.0)
    }
}

#[derive(Copy, Clone, PartialEq, Eq, Hash)]
struct PathHash(u64);

impl std::fmt::Debug for PathHash {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "PathHash({:#x})", self.0)
    }
}

impl std::fmt::Display for PathHash {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", hex::encode(&self.0.to_be_bytes()))
    }
}

impl<'de> Deserialize<'de> for PathHash {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        let bytes = hex::decode(&s).map_err(de::Error::custom)?;
        let arr: [u8; 8] = bytes.try_into().map_err(|bytes: Vec<u8>| {
            de::Error::custom(format!(
                "PathHash length mismatch: expected 8 bytes, got {}",
                bytes.len()
            ))
        })?;
        Ok(PathHash(u64::from_be_bytes(arr)))
    }
}

impl Serialize for PathHash {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&hex::encode(self.0.to_be_bytes()))
    }
}

impl<'a> TryFrom<&'a [u8]> for PathHash {
    type Error = anyhow::Error;
    fn try_from(value: &'a [u8]) -> Result<Self> {
        let arr: [u8; 8] = value.try_into()?;
        Ok(PathHash(u64::from_be_bytes(arr)))
    }
}

impl<'a> TryFrom<&'a str> for PathHash {
    type Error = anyhow::Error;
    fn try_from(value: &'a str) -> Result<Self> {
        Self::try_from(hex::decode(value)?.as_slice())
    }
}

impl PathHash {
    fn to_string(&self) -> String {
        hex::encode(&self.0.to_be_bytes())
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct KeyData {
    boot_strap: String,
    warning: String,
    #[serde(with = "hex_vec")]
    params: Vec<u8>,
    archive_unique_key: String,
    #[serde(rename = "seed", with = "hex_vec_optional", default)]
    upper_key: Option<Vec<u8>>,
}

mod hex_vec {
    use serde::{Deserialize, Deserializer, Serializer};
    pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&hex::encode(bytes))
    }
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        hex::decode(s).map_err(serde::de::Error::custom)
    }
}

mod hex_vec_optional {
    use super::hex_vec;
    use serde::{Deserialize, Deserializer, Serializer};
    pub fn serialize<S>(bytes: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match bytes {
            Some(b) => hex_vec::serialize(b, serializer),
            None => serializer.serialize_none(),
        }
    }
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s: Option<String> = Option::deserialize(deserializer)?;
        match s {
            Some(hex_str) => hex::decode(hex_str)
                .map(Some)
                .map_err(serde::de::Error::custom),
            None => Ok(None),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct KeyPackage {
    #[allow(unused)]
    description: String,
    key: KeyData,
    sku: String,
}

#[derive(Clone, Deserialize, Serialize, msg_tool_macro::Default)]
#[serde(rename_all = "camelCase")]
struct CxdecDb {
    #[allow(unused)]
    #[default("xp3hnp".into())]
    file_hash_salt: String,
    /// xp3 filename -> path hash -> file hash -> file name
    file_list: HashMap<String, HashMap<PathHash, HashMap<FileHash, Option<String>>>>,
    #[serde(default)]
    key_packages: Vec<KeyPackage>,
    #[allow(unused)]
    #[default("xp3hnp".into())]
    path_hash_salt: String,
    path_mapping: HashMap<PathHash, Option<String>>,
    project_name: String,
}

impl std::fmt::Debug for CxdecDb {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CxdecDb").finish_non_exhaustive()
    }
}

#[derive(Debug)]
pub struct HxCrypt {
    base: CxEncryption,
    key1: IndexKey,
    key2: IndexKeys,
    filter_key: AtomicU64,
    file_mapping: Arc<HashMap<FileHash, String>>,
    path_mapping: Arc<HashMap<PathHash, String>>,
    info_map: Mutex<HashMap<String, HxEntry>>,
    file_hash: FileHashOption,
    path_hash: PathHashOption,
    dump_file_hash: Option<String>,
    filename: String,
}

#[derive(Clone)]
pub struct IndexKey {
    key: [u8; 32],
    nonce: [u8; 16],
    filter_key: Option<u64>,
}

impl std::fmt::Debug for IndexKey {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("IndexKey")
            .field("key", &hex::encode(&self.key))
            .field("nonce", &hex::encode(&self.nonce))
            .field("filter_key", &self.filter_key)
            .finish()
    }
}

#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct IndexKeyTmp {
    key: String,
    nonce: String,
    #[serde(default)]
    filter_key: Option<u64>,
}

impl<'de> Deserialize<'de> for IndexKey {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        use base64::{Engine, engine::general_purpose::STANDARD};
        let s = IndexKeyTmp::deserialize(deserializer)?;
        let bytes = STANDARD.decode(&s.key).map_err(de::Error::custom)?;
        let key: [u8; 32] = bytes.try_into().map_err(|bytes: Vec<u8>| {
            de::Error::custom(format!(
                "Index key length mismatch: expected 32 bytes, got {}",
                bytes.len()
            ))
        })?;
        let hbytes = STANDARD.decode(&s.nonce).map_err(de::Error::custom)?;
        let nonce: [u8; 16] = hbytes.try_into().map_err(|bytes: Vec<u8>| {
            de::Error::custom(format!(
                "Index key nonce length mismatch: expected 16 bytes, got {}",
                bytes.len()
            ))
        })?;
        Ok(Self {
            key,
            nonce,
            filter_key: s.filter_key,
        })
    }
}

#[derive(Clone, Debug)]
pub struct IndexKeys(pub Vec<IndexKey>);

impl Deref for IndexKeys {
    type Target = Vec<IndexKey>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for IndexKeys {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

#[derive(Deserialize)]
#[serde(untagged)]
enum IndexKeysTmp {
    List(Vec<IndexKey>),
    Single(IndexKey),
}

impl<'de> Deserialize<'de> for IndexKeys {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let tmp = IndexKeysTmp::deserialize(deserializer)?;
        Ok(match tmp {
            IndexKeysTmp::List(list) => Self(list),
            IndexKeysTmp::Single(one) => Self(vec![one]),
        })
    }
}

impl HxCrypt {
    pub fn new(
        base: BaseSchema,
        cx: &CxSchema,
        index_key1: &IndexKey,
        index_key2: &IndexKeys,
        filter_key: u64,
        random_type: i32,
        file_list_name: Option<&str>,
        file_list_path: Option<&str>,
        filename: &str,
        config: &ExtraConfig,
    ) -> Result<Self> {
        let p = std::path::Path::new(filename);
        let b = p
            .file_name()
            .ok_or_else(|| anyhow::anyhow!("Failed to get file name from path."))?;
        let s: &str = &b.to_string_lossy();
        let (file_map, mut path_map) = if let Some(path) = file_list_path {
            let data = std::fs::read(path)?;
            let data = decode_to_string(Encoding::Utf8, &data, true)?;
            Self::read_names(&data, s)?
        } else if let Some(name) = file_list_name {
            let flist = query_filename_list(name)?;
            Self::read_names(&flist, s)?
        } else {
            let pdir = p.parent().map(|s| s.to_owned()).unwrap_or_default();
            if let Some(k) = Self::try_default_name(&pdir.join("filelist.json"), s)? {
                k
            } else if let Some(k) = Self::try_default_name(&pdir.join("filelist.lst"), s)? {
                k
            } else {
                (HashMap::new(), HashMap::new())
            }
        };
        let default_path_hash = calculate_path_hash("", "xp3hnp");
        if !path_map.contains_key(&default_path_hash) {
            path_map.insert(default_path_hash, String::new());
        }
        Ok(Self {
            base: CxEncryption::new_inner(
                base,
                cx,
                filename,
                Box::new(HxProgramBuilder::new(random_type)),
            )?,
            key1: index_key1.clone(),
            key2: index_key2.clone(),
            filter_key: AtomicU64::new(filter_key),
            file_mapping: Arc::new(file_map),
            path_mapping: Arc::new(path_map),
            info_map: Mutex::new(HashMap::new()),
            file_hash: config.xp3_cxdec_file_hash,
            path_hash: config.xp3_cxdec_path_hash,
            dump_file_hash: config.xp3_dump_file_hash_list.clone(),
            filename: filename.to_owned(),
        })
    }

    fn new_inner(
        base: BaseSchema,
        cx: &CxSchema,
        index_key1: IndexKey,
        index_key2: IndexKeys,
        filter_key: u64,
        random_type: i32,
        config: &ExtraConfig,
        file_mapping: Arc<HashMap<FileHash, String>>,
        path_mapping: Arc<HashMap<PathHash, String>>,
        control_block: Vec<u32>,
        filename: &str,
    ) -> Result<Self> {
        Ok(Self {
            base: CxEncryption::new_inner2(
                base,
                cx,
                Box::new(HxProgramBuilder::new(random_type)),
                control_block,
            )?,
            key1: index_key1,
            key2: index_key2,
            filter_key: AtomicU64::new(filter_key),
            file_mapping,
            path_mapping,
            info_map: Mutex::new(HashMap::new()),
            file_hash: config.xp3_cxdec_file_hash,
            path_hash: config.xp3_cxdec_path_hash,
            dump_file_hash: config.xp3_dump_file_hash_list.clone(),
            filename: filename.to_owned(),
        })
    }

    fn try_default_name<P: AsRef<std::path::Path>>(
        s: &P,
        b: &str,
    ) -> Result<Option<(HashMap<FileHash, String>, HashMap<PathHash, String>)>> {
        let n = match get_ignorecase_path(s) {
            Ok(s) => s,
            Err(_) => return Ok(None),
        };
        if !n.exists() {
            return Ok(None);
        }
        let s = std::fs::read(&n)?;
        let data = decode_to_string(Encoding::Utf8, &s, true)?;
        let names = Self::read_names(&data, b)?;
        eprintln!(
            "Read {} file entries and {} directory entries from filelist {}.",
            names.0.len(),
            names.1.len(),
            n.display()
        );
        Ok(Some(names))
    }

    fn read_names(
        s: &str,
        basename: &str,
    ) -> Result<(HashMap<FileHash, String>, HashMap<PathHash, String>)> {
        if let Ok(s) = serde_json::from_str::<CxdecDb>(&s) {
            let path_map: HashMap<_, _> = s
                .path_mapping
                .iter()
                .filter_map(|(k, v)| match v {
                    Some(v) => Some((k.clone(), v.clone())),
                    None => None,
                })
                .collect();
            let file_map: HashMap<_, _> = if let Some(s) = s.file_list.get(basename) {
                s.iter()
                    .map(|s| s.1)
                    .flatten()
                    .filter_map(|(k, v)| match v {
                        Some(v) => Some((k.clone(), v.clone())),
                        None => None,
                    })
                    .collect()
            } else {
                HashMap::new()
            };
            return Ok((file_map, path_map));
        }
        let mut file_map = HashMap::new();
        let mut path_map = HashMap::new();
        for line in s.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            let mut iter = line.splitn(2, ':');
            let key = match iter.next() {
                Some(v) => v,
                None => continue,
            };
            let value = match iter.next() {
                Some(v) => v,
                None => continue,
            };
            if key.len() == 16 {
                let key = PathHash::try_from(key)?;
                path_map.insert(key, value.to_string());
            } else if key.len() == 64 {
                let key = FileHash::try_from(key)?;
                file_map.insert(key, value.to_string());
            }
        }
        Ok((file_map, path_map))
    }

    fn create_chacha20_crypt(&self, flags: u16, key_index: usize) -> Result<ChaCha20Legacy> {
        use chacha20::{KeyIvInit, cipher::StreamCipherSeek};
        let key = match flags {
            0 => self
                .key2
                .get(key_index)
                .ok_or_else(|| anyhow::anyhow!("Index out of bound"))?,
            1 => &self.key1,
            _ => anyhow::bail!("Unknown hxv4 flags: {}", flags),
        };
        if let Some(filter_key) = &key.filter_key {
            self.filter_key.qsave(*filter_key);
        }
        let mut nonce = [0; 8];
        nonce.copy_from_slice(&key.nonce[..8]);
        let mut crypt = ChaCha20Legacy::new((&key.key).into(), (&nonce).into());
        crypt.try_seek(64)?;
        Ok(crypt)
    }

    fn read_index_stream_internel<T: Read + Seek>(
        &self,
        stream: &mut T,
        flags: u16,
        key_index: usize,
    ) -> Result<MemReader> {
        use chacha20::cipher::StreamCipher;
        stream.rewind()?;
        let len = stream.stream_length()?;
        let mut crypt = self.create_chacha20_crypt(flags, key_index)?;
        let tlen = len as usize - 16;
        let mut buf = Vec::with_capacity(tlen);
        stream.seek(SeekFrom::Start(16))?;
        stream.read_to_end(&mut buf)?;
        crypt.try_apply_keystream(&mut buf)?;
        let mut stream = flate2::read::ZlibDecoder::new(MemReaderRef::new(&buf[4..]));
        let mut buf = Vec::new();
        stream.read_to_end(&mut buf)?;
        Ok(MemReader::new(buf))
    }

    fn read_index_stream<T: Read + Seek>(&self, mut stream: T, flags: u16) -> Result<MemReader> {
        if flags != 0 {
            self.read_index_stream_internel(&mut stream, flags, 0)
        } else if self.key2.len() == 1 {
            self.read_index_stream_internel(&mut stream, flags, 0)
        } else {
            for i in 0..self.key2.len() {
                if let Ok(reader) = self.read_index_stream_internel(&mut stream, flags, i) {
                    return Ok(reader);
                }
            }
            anyhow::bail!("All index key decrypt failed.")
        }
    }

    fn read_manifest(mainfest_dir: &str) -> Result<CxdecDb> {
        Ok(serde_json::from_reader(std::fs::File::open(mainfest_dir)?)?)
    }

    fn read_index<T: Read + Seek>(&self, stream: T, flags: u16) -> Result<()> {
        let mut reader = self.read_index_stream(stream, flags)?;
        let root_obj = TjsValue::unpack(&mut reader, true, Encoding::Utf16LE, &None)?;
        if !root_obj.is_array() {
            anyhow::bail!("Index object is not an array.");
        }
        let mut info_map = self.info_map.lock_blocking();
        info_map.clear();
        let set = create_garbage_filename_set("xp3hnp");
        let basename: &str = &std::path::Path::new(&self.filename)
            .file_name()
            .unwrap_or_default()
            .to_string_lossy();
        let mut manifest = if let Some(filename) = self.dump_file_hash.as_ref() {
            Some(match Self::read_manifest(filename) {
                Ok(manifest) => manifest,
                Err(_) => CxdecDb::default(),
            })
        } else {
            None
        };
        for i in (0..root_obj.len()).step_by(2) {
            let path_hash = PathHash::try_from(
                root_obj[i]
                    .as_bytes()
                    .ok_or_else(|| anyhow::anyhow!("path hash is not bytes."))?,
            )?;
            if let Some(m) = manifest.as_mut() {
                if !m.path_mapping.contains_key(&path_hash) {
                    m.path_mapping.insert(path_hash, None);
                }
            }
            let dir_obj = &root_obj[i + 1];
            if !dir_obj.is_array() {
                anyhow::bail!("dir object at index {} is not array.", i + 1);
            }
            let (path_name, path_is_hash) = if let Some(n) = self.path_mapping.get(&path_hash) {
                (n.to_owned(), false)
            } else {
                (path_hash.to_string() + "/", true)
            };
            for j in (0..dir_obj.len()).step_by(2) {
                let entry_hash = FileHash::try_from(
                    dir_obj[j]
                        .as_bytes()
                        .ok_or_else(|| anyhow::anyhow!("entry hash is not bytes."))?,
                )?;
                if let Some(m) = manifest.as_mut() {
                    let xp3 = m.file_list.entry(basename.into()).or_default();
                    let path = xp3.entry(path_hash).or_default();
                    if !path.contains_key(&entry_hash) {
                        path.insert(entry_hash, None);
                    }
                }
                let entry_obj = &dir_obj[j + 1];
                if !entry_obj.is_array() {
                    anyhow::bail!("Entry object at index {},{} is not array.", i + 1, j + 1);
                }
                if entry_obj.len() < 2 {
                    anyhow::bail!("Entry object at index {},{} is too small.", i + 1, j + 1);
                }
                let entry_id = entry_obj[0]
                    .as_u64()
                    .ok_or_else(|| anyhow::anyhow!("Entry id is not int."))?;
                let entry_key = entry_obj[1]
                    .as_u64()
                    .ok_or_else(|| anyhow::anyhow!("Entry key is not int."))?;
                let (name, name_is_hash) = if let Some(n) = self.file_mapping.get(&entry_hash) {
                    (n.to_owned(), false)
                } else {
                    (entry_hash.to_string(), true)
                };
                let uname = Self::get_unicode_name(entry_id as u32);
                let entry = HxEntry {
                    path: path_name.clone(),
                    name,
                    id: entry_id,
                    key: entry_key,
                    name_is_hash,
                    path_is_hash,
                    is_garbage: set.contains(&entry_hash),
                };
                info_map.insert(uname, entry);
            }
        }
        if let Some(filename) = &self.dump_file_hash
            && let Some(db) = manifest
        {
            serde_json::to_writer_pretty(std::fs::File::create(filename)?, &db)?;
        }
        Ok(())
    }

    fn get_unicode_name(mut hash: u32) -> String {
        let mut buf = [0u16; 4];
        let mut i = 0;
        loop {
            buf[i] = ((hash & 0x3FFF) + 0x5000) as u16;
            hash >>= 14;
            i += 1;
            if hash == 0 {
                break;
            }
        }
        let s = String::from_utf16_lossy(&buf[..i]);
        s
    }

    fn create_filter_key(&self, entry_key: u64, header_key_seed: u64) -> Result<HxFilterKey> {
        let key0 = entry_key as u32;
        let key1 = (entry_key >> 32) as u32;
        let k0 = self.base.execute_xcode(key0)?;
        let file_key_0 = (k0.0 as u64) | ((k0.1 as u64) << 32);
        let k1 = self.base.execute_xcode(key1)?;
        let file_key_1 = (k1.0 as u64) | ((k1.1 as u64) << 32);
        let split_position =
            (self.base.offset as u64 + ((entry_key >> 16) & self.base.mask as u64)) & 0xffffffff;
        let mut header_key = [0u8; 16];
        let k3 = self.base.execute_xcode(header_key_seed as u32)?;
        let mut v5 = (k3.0 as u64) | ((k3.1 as u64) << 32);
        v5 = !v5;
        let mut writer = MemWriterRef::new(&mut header_key);
        writer.write_u64_be(v5)?;
        let k3 = self.base.execute_xcode(v5 as u32)?;
        v5 = (k3.0 as u64) | ((k3.1 as u64) << 32);
        v5 = !v5;
        writer.write_u64_be(v5)?;
        Ok(HxFilterKey {
            key: [file_key_0, file_key_1],
            header_key,
            split_position,
            has_header_key: true,
            flag: false,
        })
    }
}

impl AsRef<CxEncryption> for HxCrypt {
    fn as_ref(&self) -> &CxEncryption {
        &self.base
    }
}

struct CopyStream<'a> {
    inner: Box<dyn Read + Send + Sync + 'a>,
}

impl<'a> CopyStream<'a> {
    pub fn new(stream: Box<dyn Read + Send + Sync + 'a>) -> Self {
        Self { inner: stream }
    }
}

impl<'a> std::fmt::Debug for CopyStream<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CopyStream").finish_non_exhaustive()
    }
}

impl<'a> Read for CopyStream<'a> {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        self.inner.read(buf)
    }
}

impl Crypt for HxCrypt {
    base_schema_impl!();
    fn init(&self, archive: &mut Xp3Archive) -> Result<()> {
        if let Some(hxv4) = archive.extras.iter().find(|x| x.tag == "Hxv4") {
            let mut reader = MemReaderRef::new(&hxv4.data);
            let offset = reader.read_u64()? + archive.base_offset;
            let size = reader.read_u32()?;
            let flags = reader.read_u16()?;
            let stream = StreamRegion::with_size(
                MutexWrapper::new(archive.inner.clone(), offset),
                size as u64,
            )?;
            self.read_index(stream, flags)?;
            let info_map = self.info_map.lock_blocking();
            for entry in archive.entries.iter_mut() {
                if let Some(info) = info_map.get(&entry.name) {
                    if info.is_garbage {
                        continue;
                    }
                    if self.path_hash == PathHashOption::Both || !info.path_is_hash {
                        entry.name = format!("{}{}", info.path, info.name);
                    } else {
                        entry.name = info.name.clone();
                    }
                    let info = info.clone();
                    entry.extra = Some(Arc::new(Box::new(info)))
                }
            }
            archive.entries.retain(|x| {
                x.extra.is_some() || !info_map.get(&x.name).is_some_and(|x| x.is_garbage)
            });
            if self.file_hash == FileHashOption::WithName {
                archive.entries.retain(|x| {
                    !(x.extra.as_ref().is_some_and(|x| {
                        x.as_any()
                            .downcast_ref::<HxEntry>()
                            .is_some_and(|x| x.name_is_hash)
                    }))
                });
            } else if self.file_hash == FileHashOption::WithoutName {
                archive.entries.retain(|x| {
                    x.extra.as_ref().is_some_and(|x| {
                        x.as_any()
                            .downcast_ref::<HxEntry>()
                            .is_some_and(|x| x.name_is_hash)
                    })
                });
            }
        }
        archive.extras.retain(|x| x.tag != "Hxv4");
        Ok(())
    }
    fn decrypt_supported(&self) -> bool {
        true
    }
    fn decrypt_seek_supported(&self) -> bool {
        true
    }
    fn decrypt<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn Read + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadDebug + Send + Sync + 'a>> {
        let info = match entry.extra.as_ref() {
            Some(info) => info,
            None => return Ok(Box::new(CopyStream::new(stream))),
        };
        let info = info
            .as_any()
            .downcast_ref::<HxEntry>()
            .ok_or_else(|| anyhow::anyhow!("extra info is not hx entry."))?;
        let mut entry_key = info.key;
        if (info.id & 0x100000000) == 0 {
            entry_key ^= self.filter_key.qload();
        }
        let header_key = !entry_key;
        let key = self.create_filter_key(entry_key, header_key)?;
        let filter = HxFilter::new(key);
        let key = (
            entry.file_hash,
            Box::new(filter) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
    fn decrypt_with_seek<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn ReadSeek + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
        let info = match entry.extra.as_ref() {
            Some(info) => info,
            None => return Ok(stream),
        };
        let info = info
            .as_any()
            .downcast_ref::<HxEntry>()
            .ok_or_else(|| anyhow::anyhow!("extra info is not hx entry."))?;
        let mut entry_key = info.key;
        if (info.id & 0x100000000) == 0 {
            entry_key ^= self.filter_key.qload();
        }
        let header_key = !entry_key;
        let key = self.create_filter_key(entry_key, header_key)?;
        let filter = HxFilter::new(key);
        let key = (
            entry.file_hash,
            Box::new(filter) as Box<dyn ICxEncryption + Send + Sync + 'a>,
        );
        Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
    }
}

#[derive(Debug)]
enum TjsValue {
    Void,
    Str(String),
    ByteArray(Vec<u8>),
    Int(i64),
    #[allow(unused)]
    Double(f64),
    Array(Vec<TjsValue>),
    Dict(HashMap<String, TjsValue>),
}

impl TjsValue {
    fn is_array(&self) -> bool {
        matches!(self, Self::Array(_))
    }

    fn len(&self) -> usize {
        match self {
            Self::Str(s) => s.len(),
            Self::ByteArray(arr) => arr.len(),
            Self::Array(arr) => arr.len(),
            Self::Dict(dict) => dict.len(),
            _ => 0,
        }
    }

    fn as_bytes(&self) -> Option<&[u8]> {
        match self {
            Self::ByteArray(arr) => Some(arr),
            _ => None,
        }
    }

    fn as_u64(&self) -> Option<u64> {
        match self {
            Self::Int(i) => Some(*i as u64),
            _ => None,
        }
    }
}

const VOID: TjsValue = TjsValue::Void;

impl Index<usize> for TjsValue {
    type Output = TjsValue;
    fn index(&self, index: usize) -> &Self::Output {
        match self {
            Self::Array(arr) => arr.get(index).unwrap_or(&VOID),
            _ => &VOID,
        }
    }
}

fn unpack_string<R: Read + Seek>(reader: &mut R, big: bool, encoding: Encoding) -> Result<String> {
    let len = u32::unpack(reader, big, encoding, &None)? as usize;
    let tlen = if encoding.is_utf16le() { len * 2 } else { len };
    let mut buf = vec![0u8; tlen];
    reader.read_exact(&mut buf)?;
    let s = decode_to_string(encoding, &buf, true)?;
    Ok(s)
}

impl StructUnpack for TjsValue {
    fn unpack<R: Read + Seek>(
        reader: &mut R,
        big: bool,
        encoding: Encoding,
        info: &Option<Box<dyn std::any::Any>>,
    ) -> Result<Self> {
        let typ = u8::unpack(reader, big, encoding, info)?;
        Ok(match typ {
            0 => Self::Void,
            2 => Self::Str(unpack_string(reader, big, encoding)?),
            3 => {
                let len = u32::unpack(reader, big, encoding, info)?;
                let data = reader.read_exact_vec(len as usize)?;
                Self::ByteArray(data)
            }
            4 => {
                let num = i64::unpack(reader, big, encoding, info)?;
                Self::Int(num)
            }
            5 => {
                let num = f64::unpack(reader, big, encoding, info)?;
                Self::Double(num)
            }
            0x81 => {
                let len = u32::unpack(reader, big, encoding, info)?;
                let mut arr = Vec::with_capacity(len as usize);
                for _ in 0..len {
                    arr.push(Self::unpack(reader, big, encoding, info)?);
                }
                Self::Array(arr)
            }
            0xC1 => {
                let len = u32::unpack(reader, big, encoding, info)?;
                let mut dict = HashMap::with_capacity(len as usize);
                for _ in 0..len {
                    let name = unpack_string(reader, big, encoding)?;
                    let obj = Self::unpack(reader, big, encoding, info)?;
                    dict.insert(name, obj);
                }
                Self::Dict(dict)
            }
            _ => anyhow::bail!("Unknown type id: {typ:02x}."),
        })
    }
}

#[derive(Clone, Debug)]
struct HxEntry {
    path: String,
    name: String,
    id: u64,
    key: u64,
    name_is_hash: bool,
    path_is_hash: bool,
    is_garbage: bool,
}

impl AnyDebug for HxEntry {
    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
struct HxSplitMix64 {
    state: u64,
}

impl HxSplitMix64 {
    pub fn new(seed: u64) -> Self {
        Self { state: seed }
    }
}

trait IRng: std::fmt::Debug {
    fn next(&mut self) -> u64;
}

impl IRng for HxSplitMix64 {
    fn next(&mut self) -> u64 {
        self.state = self.state.wrapping_add(0x9E3779B97F4A7C15);
        let mut z = self.state;
        z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
        z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
        z ^ (z >> 31)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Xoroshiro128PlusPlus {
    state: [u64; 2],
}

impl Xoroshiro128PlusPlus {
    pub fn new(state: [u64; 2]) -> Self {
        assert!(
            state[0] != 0 || state[1] != 0,
            "Initial state cannot be all zeros."
        );
        Self { state }
    }
}

impl IRng for Xoroshiro128PlusPlus {
    fn next(&mut self) -> u64 {
        let s0 = self.state[0];
        let mut s1 = self.state[1];
        let result = s0.wrapping_add(s1).rotate_left(17).wrapping_add(s0);
        s1 ^= s0;
        self.state[0] = s0.rotate_left(49) ^ s1 ^ (s1 << 21);
        self.state[1] = s1.rotate_left(28);
        result
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Xoroshiro128StarStar {
    state: [u64; 2],
}

impl Xoroshiro128StarStar {
    pub fn new(state: [u64; 2]) -> Self {
        assert!(
            state[0] != 0 || state[1] != 0,
            "Initial state cannot be all zeros."
        );
        Self { state }
    }
}

impl IRng for Xoroshiro128StarStar {
    fn next(&mut self) -> u64 {
        let s0 = self.state[0];
        let mut s1 = self.state[1];
        let result = s0.wrapping_mul(5).rotate_left(7).wrapping_mul(9);
        s1 ^= s0;
        self.state[0] = s0.rotate_left(24) ^ s1 ^ (s1 << 16);
        self.state[1] = s1.rotate_left(37);
        result
    }
}

#[derive(Debug)]
struct HxProgram {
    base: CxProgram,
    rng: Box<dyn IRng + Send + Sync>,
}

impl HxProgram {
    pub fn new(seed: u32, control_block: Weak<Vec<u32>>, random_method: i32) -> Self {
        let initial_seed = (seed as u64) | (!(seed as u64) << 32);
        let mut seeder = HxSplitMix64::new(initial_seed);
        let seed1 = seeder.next();
        let seed2 = seeder.next();
        let xoroshiro_seed = [seed1, seed2];
        Self {
            base: CxProgram {
                code: Vec::new(),
                control_block,
                length: 0,
                seed,
            },
            rng: if random_method == 0 {
                Box::new(Xoroshiro128PlusPlus::new(xoroshiro_seed))
            } else {
                Box::new(Xoroshiro128StarStar::new(xoroshiro_seed))
            },
        }
    }
}

impl ICxProgram for HxProgram {
    fn execute(&self, hash: u32) -> Result<u32> {
        self.base.execute(hash)
    }
    fn clear(&mut self) {
        self.base.clear();
    }
    fn emit(&mut self, bytecode: CxByteCode, length: usize) -> bool {
        self.base.emit(bytecode, length)
    }
    fn emit_nop(&mut self, count: usize) -> bool {
        self.base.emit_nop(count)
    }
    fn emit_u32(&mut self, x: u32) -> bool {
        self.base.emit_u32(x)
    }
    fn get_random(&mut self) -> u32 {
        self.rng.next() as u32
    }
}

#[derive(Debug)]
struct HxProgramBuilder {
    random_method: i32,
}

impl HxProgramBuilder {
    pub fn new(random_method: i32) -> Self {
        Self { random_method }
    }
}

impl ICxProgramBuilder for HxProgramBuilder {
    fn build(
        &self,
        seed: u32,
        control_blocks: Weak<Vec<u32>>,
    ) -> Box<dyn ICxProgram + Send + Sync> {
        Box::new(HxProgram::new(seed, control_blocks, self.random_method))
    }
}

struct HxFilterKey {
    key: [u64; 2],
    header_key: [u8; 16],
    split_position: u64,
    has_header_key: bool,
    flag: bool,
}

#[derive(Clone)]
struct HxFilterSpanDecryptor {
    first_decrypt_key: u32,
    key1: u8,
    key2: u8,
    span_position: [u64; 2],
}

impl std::fmt::Debug for HxFilterSpanDecryptor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("HxFilterSpanDecryptor")
            .field(
                "firstDecryptKey",
                &format_args!("{:#x}", &self.first_decrypt_key),
            )
            .field("key1", &format_args!("{:#x}", &self.key1))
            .field("key2", &format_args!("{:#x}", &self.key2))
            .field(
                "spanPosition",
                &format_args!("{:#x}, {:#x}", self.span_position[0], self.span_position[1]),
            )
            .finish()
    }
}

impl HxFilterSpanDecryptor {
    pub fn new(key: u64, flag: bool) -> Self {
        let decrypt_key_bytes = ((key >> 8) & 0xFFFF) as u32;
        let mut first_decrypt_key = (key & 0xFF) as u32;
        let mut span_position = [(key >> 48) & 0xFFFF, (key >> 32) & 0xFFFF];
        if span_position[0] == span_position[1] {
            span_position[1] = span_position[1].wrapping_add(1);
        }
        if !flag && first_decrypt_key == 0 {
            first_decrypt_key = 0xA5;
        }
        first_decrypt_key = first_decrypt_key.wrapping_mul(0x01010101);
        let (key1, key2) = if flag {
            (0, 0)
        } else {
            (
                (decrypt_key_bytes & 0xFF) as u8,
                ((decrypt_key_bytes >> 8) & 0xFF) as u8,
            )
        };
        Self {
            first_decrypt_key,
            key1,
            key2,
            span_position,
        }
    }

    pub fn decrypt(&self, position: u64, data: &mut [u8]) {
        if data.is_empty() {
            return;
        }
        let key_bytes = self.first_decrypt_key.to_le_bytes();
        for (i, byte) in data.iter_mut().enumerate() {
            let key_index = ((position as usize) + i) & 3;
            *byte ^= key_bytes[key_index];
        }
        let data_len = data.len() as u64;
        if self.key1 != 0 {
            let pos1 = self.span_position[0];
            if pos1 >= position && pos1 < position + data_len {
                let index = (pos1 - position) as usize;
                data[index] ^= self.key1;
            }
        }
        if self.key2 != 0 {
            let pos2 = self.span_position[1];
            if pos2 >= position && pos2 < position + data_len {
                let index = (pos2 - position) as usize;
                data[index] ^= self.key2;
            }
        }
    }
}

struct HxFilter {
    span_decryptors: [HxFilterSpanDecryptor; 2],
    split_position: u64,
    header_key: [u8; 16],
    has_header_key: bool,
}

impl HxFilter {
    pub fn new(key: HxFilterKey) -> HxFilter {
        HxFilter {
            span_decryptors: [
                HxFilterSpanDecryptor::new(key.key[0], key.flag),
                HxFilterSpanDecryptor::new(key.key[1], key.flag),
            ],
            split_position: key.split_position,
            header_key: key.header_key,
            has_header_key: key.has_header_key,
        }
    }

    fn decrypt_header(&self, position: u64, buffer: &mut [u8]) {
        let header_len = self.header_key.len() as u64;
        let overlap_start = position;
        let overlap_end = (position + buffer.len() as u64).min(header_len);
        if overlap_start >= overlap_end {
            return;
        }
        for i in overlap_start..overlap_end {
            let buffer_index = (i - position) as usize;
            let key_index = i as usize;
            buffer[buffer_index] ^= self.header_key[key_index];
        }
    }

    pub fn decrypt(&self, position: u64, buffer: &mut [u8]) {
        if buffer.is_empty() {
            return;
        }
        if self.has_header_key {
            self.decrypt_header(position, buffer);
        }
        let buffer_len = buffer.len() as u64;
        let buffer_end_pos = position + buffer_len;
        if buffer_end_pos <= self.split_position {
            self.span_decryptors[0].decrypt(position, buffer);
        } else if position >= self.split_position {
            self.span_decryptors[1].decrypt(position, buffer);
        } else {
            let split_index = (self.split_position - position) as usize;
            let (part1, part2) = buffer.split_at_mut(split_index);
            self.span_decryptors[0].decrypt(position, part1);
            self.span_decryptors[1].decrypt(self.split_position, part2);
        }
    }
}

impl std::fmt::Debug for HxFilter {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("HxFilter")
            .field("spanDecryptors", &self.span_decryptors)
            .field(
                "splitPosition",
                &format_args!("{:#x}", &self.split_position),
            )
            .field("hasHeaderKey", &self.has_header_key)
            .field("headerKey", &hex::encode(self.header_key))
            .finish()
    }
}

impl ICxEncryption for HxFilter {
    fn get_base_offset(&self, _hash: u32) -> u32 {
        0
    }
    fn decode(
        &self,
        _key: u32,
        _offset: u64,
        _buffer: &mut [u8],
        _pos: usize,
        _count: usize,
    ) -> Result<()> {
        Ok(())
    }
    fn inner_decrypt(
        &self,
        _key: u32,
        offset: u64,
        buffer: &mut [u8],
        pos: usize,
        count: usize,
    ) -> Result<()> {
        self.decrypt(offset, &mut buffer[pos..pos + count]);
        Ok(())
    }
}

fn calculate_file_hash(pathname: &str, file_hash_salt: &str) -> FileHash {
    use blake2::{Blake2s256, Digest};
    let mut hasher = Blake2s256::new();
    (pathname.to_lowercase() + file_hash_salt)
        .encode_utf16()
        .for_each(|b| {
            hasher.update(&b.to_le_bytes());
        });
    let result = hasher.finalize();
    let mut hash = [0u8; 32];
    hash.copy_from_slice(&result);
    FileHash(hash)
}

fn create_garbage_filename_set(file_hash_salt: &str) -> HashSet<FileHash> {
    let mut set = HashSet::new();
    set.insert(calculate_file_hash("$$$ This is a protected archive. $$$ 著作者はこのアーカイブが正規の利用方法以外の方法で展開されることを望んでいません。 $$$ This is a protected archive. $$$ 著作者はこのアーカイブが正規の利用方法以外の方法で展開されることを望んでいません。 $$$ This is a protected archive. $$$ 著作者はこのアーカイブが正規の利用方法以外の方法で展開されることを望んでいません。 $$$ Warning! Extracting this archive may infringe on author's rights. 警告 このアーカイブを展開することにより、あなたは著作者の権利を侵害するおそれがあります。.txt", file_hash_salt));
    set
}

fn calculate_path_hash(pathname: &str, path_hash_salt: &str) -> PathHash {
    use std::hash::Hasher;
    let mut hasher = siphasher::sip::SipHasher24::new();
    (pathname.to_lowercase() + path_hash_salt)
        .encode_utf16()
        .for_each(|b| {
            hasher.write(&b.to_le_bytes());
        });
    let data = hasher.finish().to_ne_bytes();
    PathHash(u64::from_be_bytes(data))
}

fn triple32(mut v: u32) -> u32 {
    v ^= v >> 17;
    v = v.wrapping_mul(0xED5AD4BB);
    v ^= v >> 11;
    v = v.wrapping_mul(0xAC4C1B51);
    v ^= v >> 15;
    v = v.wrapping_mul(0x31848BAB);
    v ^= v >> 14;

    v
}

fn fnv_blake(data: &[u8], fnvbase: u32) -> Vec<u8> {
    use blake2::{Blake2s256, Digest};
    let fnv_value = (0x811C9DC5u32 ^ fnvbase).wrapping_mul(0x01000193);
    let mut hash_value = fnv_value;
    let mut out = [0u8; 32];
    for (i, &byte) in data.iter().enumerate() {
        hash_value = triple32(hash_value ^ (byte as u32));
        let offset = (i * 4) % 32;
        let hash_bytes = hash_value.to_le_bytes();
        for j in 0..4 {
            out[offset + j] ^= hash_bytes[j];
        }
    }
    let mut hasher = Blake2s256::new();
    hasher.update(data);
    hasher.update(out);

    hasher.finalize().to_vec()
}

/// Hx table keys
#[derive(Clone, Copy, msg_tool_macro::Default)]
pub struct HxKeys {
    pub key: [u8; 32],
    pub nonce_a: [u8; 32],
    pub nonce_b: [u8; 32],
    /// FilterKey
    pub filter: [u8; 8],
    /// ControlBlock
    #[default([0; 0x1000])]
    pub ctrlblk: [u8; 0x1000],
}

/// Represents the configuration parameters for the WAMSOFT Cx encryption scheme.
///
/// This structure corresponds to the 0x20-byte memory block found at `ecx + 0x3020`
/// (or passed to `CxLoadParams`). It contains the permutation tables for VM instruction
/// generation, algorithm selection flags, and encryption constants.
#[repr(C)]
#[derive(Clone, Copy, Default, StructUnpack)]
pub struct CxParams {
    /// The permutation table for the "Even" branch of the VM code generation.
    /// Maps to `EvenBranchOrder` on GarBRO (Case 0-7).
    pub even_branch_perm: [u8; 8],
    /// The permutation table for the "Odd" branch of the VM code generation.
    /// Maps to `OddBranchOrder` on GarBRO (Case 0-5).
    pub odd_branch_perm: [u8; 6],
    /// The permutation table for the Prologue of the VM code generation.
    /// Maps to `PrologOrder` on GarBRO (Case 0-2).
    pub prologue_perm: [u8; 3],
    /// Configuration flags.
    ///
    /// - Bit 7 (0x80): Random Algorithm Type (0 = Xoroshiro128PlusPlus, 1 = Xoroshiro128StarStar).
    /// - Bit 0 (0x01): ControlBlock generate mode (0 => NOP, 1 => XOR)
    pub flags: u8,
    /// The encryption mask used in the key calculation.
    pub mask: u16,
    /// The encryption offset used in the key calculation.
    pub offset: u16,
}

impl CxParams {
    pub fn new(src: &[u8]) -> Result<CxParams> {
        let mut reader = MemReaderRef::new(src);
        Self::unpack(&mut reader, false, Encoding::Utf8, &None)
    }
}

impl HxKeys {
    pub fn new(
        bootstrap: &[u8],           // get from bootstrap.tjc
        warning: &[u8],             // get from cxdec.tpm
        param: &[u8],               // get from cxdec.tpm
        uniq: &[u8],                // get from cxdec.tpm
        upper_key: Option<[u8; 8]>, // get from cxdec.tpm
    ) -> Result<(HxKeys, CxParams)> {
        use anyhow::Error;
        use argon2::{self, Algorithm, Argon2, Version};
        use sha3::Digest;
        use shake::digest::XofReader;
        // workaround for seed = defaults (if cxdec:word_10081744 == 'f')
        // consider make .keyPackages[].key.seed nullable
        //
        const DEFAULT_SEED: [u8; 8] = [0xCE, 0xEA, 0xAF, 0x2C, 0xEF, 0xBE, 0xAD, 0xDE]; // 0xDEADBEEF2CAFEACEuLL
        let upper_key = if let Some(upper_key) = upper_key {
            match u64::from_le_bytes(upper_key) == 0 {
                true => DEFAULT_SEED,
                false => upper_key,
            }
        } else {
            DEFAULT_SEED
        };
        let params = CxParams::new(param)?; // will returned later for convenience
        let bootstrap_and_warning: Vec<u8> = [bootstrap, warning].concat();
        let mut hasher = Sha3_224::new();
        hasher.update(param);
        let params_hash = &hasher.finalize()[..16];
        let mut lower_key_full = vec![0u8; 64];

        // m=8KB, t=3, p=1, len=64
        let argon2 = Argon2::new(
            Algorithm::Argon2i,
            Version::V0x13,
            argon2::Params::new(8, 3, 1, Some(64)).map_err(Error::msg)?,
        );
        argon2
            .hash_password_into(&bootstrap_and_warning, params_hash, &mut lower_key_full)
            .map_err(Error::msg)?;
        let lower_key = &lower_key_full[..32];
        let fnvbase_bytes: [u8; 4] = upper_key[0..4].try_into().map_err(Error::msg)?;
        let fnvbase = u32::from_le_bytes(fnvbase_bytes);
        let upper_key = fnv_blake(&upper_key, fnvbase);
        let b0 = fnv_blake(&bootstrap_and_warning, 0);
        let b1 = fnv_blake(param, 1);
        let b2 = fnv_blake(uniq, 2);
        let mut key_buffer: Vec<u8> = [b0, b1, b2].concat();

        if key_buffer.len() < 96 {
            return Err(anyhow::anyhow!(
                "Concatenated fnv_blake buffers are less than 96 bytes"
            ));
        }

        for i in 0..64 {
            key_buffer[i] ^= lower_key[i % 32];
        }
        for i in 0..32 {
            key_buffer[64 + i] ^= upper_key[i];
        }

        // generate control block
        let mut ctrlblk_pa = vec![0u8; 0x1000]; // cx->ControlBlock, partA
        let mut ctrlblk_pb = vec![0u8; 0x1000]; // cx->ControlBlock, partB
        let mut state = Shake256::default();
        shake::digest::Update::update(&mut state, &lower_key_full);
        let mut reader = shake::digest::ExtendableOutput::finalize_xof(state);
        reader.read(&mut ctrlblk_pa);
        reader.read(&mut ctrlblk_pb);
        if (params.flags & 1) == 1 {
            ctrlblk_pa
                .iter_mut()
                .zip(ctrlblk_pb.iter())
                .for_each(|(dst, src)| {
                    *dst ^= *src;
                });
        }

        Ok((
            HxKeys {
                key: key_buffer[0..32].try_into().map_err(Error::msg)?,
                nonce_a: key_buffer[32..64].try_into().map_err(Error::msg)?,
                nonce_b: key_buffer[64..96].try_into().map_err(Error::msg)?,
                filter: key_buffer[64..72].try_into().map_err(Error::msg)?,
                ctrlblk: ctrlblk_pa.try_into().unwrap(),
            },
            params,
        ))
    }
}

const RANDOM_TYPE_FLAG: u8 = 0x80;

fn hxkeys_new(
    bootstrap: &str,
    warning: &str,
    param: &[u8],
    uniq: &str,
    upper_key: Option<[u8; 8]>,
) -> Result<(HxKeys, CxParams)> {
    let bootstrap = encode_string(Encoding::Utf16LE, bootstrap, true)?;
    let warning = encode_string(Encoding::Utf16LE, warning, true)?;
    let uniq = encode_string(Encoding::Utf16LE, uniq, true)?;
    HxKeys::new(&bootstrap, &warning, param, &uniq, upper_key)
}

fn map_key_to_garbro(cx: &mut CxParams) {
    const S3: [u8; 3] = [0, 1, 2];
    const S6: [u8; 6] = [2, 5, 3, 4, 1, 0];
    const S8: [u8; 8] = [0, 2, 3, 1, 5, 6, 7, 4];
    let mut o3 = [0; 3];
    let mut o6 = [0; 6];
    let mut o8 = [0; 8];
    for i in 0..3 {
        o3[cx.prologue_perm[i] as usize] = S3[i];
    }
    for i in 0..6 {
        o6[cx.odd_branch_perm[i] as usize] = S6[i];
    }
    for i in 0..8 {
        o8[cx.even_branch_perm[i] as usize] = S8[i];
    }
    cx.prologue_perm.copy_from_slice(&o3);
    cx.odd_branch_perm.copy_from_slice(&o6);
    cx.even_branch_perm.copy_from_slice(&o8);
}

fn gen_index_keys(key: &HxKeys) -> Result<(IndexKey, IndexKey)> {
    let subkeya = hchacha::<chacha20::R20>(&key.key.into(), &Array::try_from(&key.nonce_a[0..16])?);
    let subkeyb = hchacha::<chacha20::R20>(&key.key.into(), &Array::try_from(&key.nonce_b[0..16])?);
    Ok((
        IndexKey {
            key: subkeya.into(),
            nonce: (&key.nonce_a[16..]).try_into()?,
            filter_key: None,
        },
        IndexKey {
            key: subkeyb.into(),
            nonce: (&key.nonce_b[16..]).try_into()?,
            filter_key: None,
        },
    ))
}

#[derive(MyDebug)]
pub struct Hxv4Crypt {
    base: Mutex<Option<HxCrypt>>,
    file_mapping: Arc<HashMap<FileHash, String>>,
    path_mapping: Arc<HashMap<PathHash, String>>,
    key_packages: Vec<KeyPackage>,
    project: String,
    #[skip_fmt]
    config: ExtraConfig,
    filename: String,
}

impl Hxv4Crypt {
    pub fn new(filename: &str, config: &ExtraConfig) -> Result<Self> {
        let p = std::path::Path::new(filename);
        let b = p
            .file_name()
            .ok_or_else(|| anyhow::anyhow!("Failed to get file name from path."))?;
        let s: &str = &b.to_string_lossy();
        let pdir = p
            .parent()
            .map(|s| s.to_owned())
            .filter(|s| !s.as_os_str().is_empty())
            .unwrap_or_else(|| PathBuf::from("."));
        let filep = get_ignorecase_path(&pdir.join("filelist.json"))?;
        let data = match std::fs::read(&filep) {
            Ok(data) => data,
            Err(err) => {
                let keys = load_key_packages_from_exe(&pdir);
                if keys.is_empty() {
                    return Err(err.into());
                }
                eprintln!("Loaded {} key packages from game exe.", keys.len());
                return Self::new2(&keys, "Unknown", filename, config);
            }
        };
        let data = decode_to_string(Encoding::Utf8, &data, true)?;
        let mut manifest = serde_json::from_str::<CxdecDb>(&data)?;
        let mut path_map: HashMap<_, _> = manifest
            .path_mapping
            .iter()
            .filter_map(|(k, v)| match v {
                Some(v) => Some((k.clone(), v.clone())),
                None => None,
            })
            .collect();
        let file_map: HashMap<_, _> = if let Some(s) = manifest.file_list.get(s) {
            s.iter()
                .map(|s| s.1)
                .flatten()
                .filter_map(|(k, v)| match v {
                    Some(v) => Some((k.clone(), v.clone())),
                    None => None,
                })
                .collect()
        } else {
            HashMap::new()
        };
        eprintln!(
            "Read {} file entries, {} directory entries and {} key packages from filelist {}.",
            file_map.len(),
            path_map.len(),
            manifest.key_packages.len(),
            filep.display()
        );
        let more_keys = load_key_packages_from_exe(&pdir);
        if !more_keys.is_empty() {
            eprintln!("Loaded {} key packages from game exe.", more_keys.len());
            manifest.key_packages.extend_from_slice(&more_keys);
        }
        let default_path_hash = calculate_path_hash("", "xp3hnp");
        if !path_map.contains_key(&default_path_hash) {
            path_map.insert(default_path_hash, String::new());
        }
        Ok(Self {
            base: Mutex::new(None),
            file_mapping: Arc::new(file_map),
            path_mapping: Arc::new(path_map),
            key_packages: manifest.key_packages,
            project: manifest.project_name,
            config: config.clone(),
            filename: filename.to_owned(),
        })
    }

    pub fn new2(
        key_packages: &[KeyPackage],
        project: &str,
        filename: &str,
        config: &ExtraConfig,
    ) -> Result<Self> {
        let p = std::path::Path::new(filename);
        let b = p
            .file_name()
            .ok_or_else(|| anyhow::anyhow!("Failed to get file name from path."))?;
        let s: &str = &b.to_string_lossy();
        let (file_map, mut path_map) = if let Some(path) = config.xp3_file_list_path.as_ref() {
            let data = std::fs::read(path)?;
            let data = decode_to_string(Encoding::Utf8, &data, true)?;
            HxCrypt::read_names(&data, s)?
        } else {
            let pdir = p.parent().map(|s| s.to_owned()).unwrap_or_default();
            if let Some(k) = HxCrypt::try_default_name(&pdir.join("filelist.json"), s)? {
                k
            } else if let Some(k) = HxCrypt::try_default_name(&pdir.join("filelist.lst"), s)? {
                k
            } else {
                (HashMap::new(), HashMap::new())
            }
        };
        let default_path_hash = calculate_path_hash("", "xp3hnp");
        if !path_map.contains_key(&default_path_hash) {
            path_map.insert(default_path_hash, String::new());
        }
        Ok(Self {
            base: Mutex::new(None),
            file_mapping: Arc::new(file_map),
            path_mapping: Arc::new(path_map),
            key_packages: key_packages.to_vec(),
            project: project.to_owned(),
            config: config.clone(),
            filename: filename.to_owned(),
        })
    }

    fn load_package(&self, pack: &KeyPackage, archive: &mut Xp3Archive) -> Result<HxCrypt> {
        eprintln!("try key {} for {}", pack.sku, self.project);
        let upper_key = match &pack.key.upper_key {
            Some(key) => Some(key.as_slice().try_into()?),
            None => None,
        };
        let (key, mut params) = hxkeys_new(
            &pack.key.boot_strap,
            &pack.key.warning,
            &pack.key.params,
            &pack.key.archive_unique_key,
            upper_key,
        )?;
        map_key_to_garbro(&mut params);
        let (key1, key2) = gen_index_keys(&key)?;
        let base = BaseSchema {
            hash_after_crypt: false,
            startup_tjs_not_encrypted: false,
            obfuscated_index: false,
        };
        let cx = CxSchema {
            mask: params.mask as u32,
            offset: params.offset as u32,
            prolog_order: Base64Bytes {
                bytes: params.prologue_perm.to_vec(),
            },
            odd_branch_order: Base64Bytes {
                bytes: params.odd_branch_perm.to_vec(),
            },
            even_branch_order: Base64Bytes {
                bytes: params.even_branch_perm.to_vec(),
            },
            control_block_name: None,
            tpm_file_name: None,
        };
        let key2 = IndexKeys(vec![key2]);
        let filter_key = u64::from_le_bytes(key.filter);
        let random_type = if params.flags & RANDOM_TYPE_FLAG != 0 {
            1
        } else {
            0
        };
        let mut control_block = Vec::with_capacity(0x400);
        let mut reader = MemReaderRef::new(&key.ctrlblk);
        for _ in 0..0x400 {
            control_block.push(!reader.read_u32()?);
        }
        let crypt = HxCrypt::new_inner(
            base,
            &cx,
            key1,
            key2,
            filter_key,
            random_type,
            &self.config,
            self.file_mapping.clone(),
            self.path_mapping.clone(),
            control_block,
            &self.filename,
        )?;
        crypt.init(archive)?;
        Ok(crypt)
    }
}

impl Crypt for Hxv4Crypt {
    fn startup_tjs_not_encrypted(&self) -> bool {
        false
    }
    fn obfuscated_index(&self) -> bool {
        false
    }
    fn hash_after_crypt(&self) -> bool {
        false
    }
    fn init(&self, archive: &mut Xp3Archive) -> Result<()> {
        if self.key_packages.len() == 0 {
            eprintln!("WARNING: No key package specifed. Decrypt not works.");
            crate::COUNTER.inc_warning();
            return Ok(());
        }
        for package in self.key_packages.iter() {
            if let Ok(crypt) = self.load_package(package, archive) {
                let mut c = self.base.lock_blocking();
                c.replace(crypt);
                return Ok(());
            }
        }
        Err(anyhow::anyhow!("Failed to decrypt index."))
    }
    fn decrypt_supported(&self) -> bool {
        true
    }
    fn decrypt_seek_supported(&self) -> bool {
        true
    }
    fn decrypt<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn Read + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadDebug + Send + Sync + 'a>> {
        if self.key_packages.len() == 0 {
            return Ok(Box::new(CopyStream::new(stream)));
        }
        let c = self.base.lock_blocking();
        let crypt = c
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("Archive not inited."))?;
        crypt.decrypt(entry, cur_seg, stream)
    }
    fn decrypt_with_seek<'a>(
        &self,
        entry: &Xp3Entry,
        cur_seg: &Segment,
        stream: Box<dyn ReadSeek + Send + Sync + 'a>,
    ) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
        if self.key_packages.len() == 0 {
            return Ok(stream);
        }
        let c = self.base.lock_blocking();
        let crypt = c
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("Archive not inited."))?;
        crypt.decrypt_with_seek(entry, cur_seg, stream)
    }
}

// Ported from https://github.com/hktkqj/cxdec-hxv4-static-analysis
fn load_key_packages_from_exe<S: AsRef<std::path::Path> + ?Sized>(path: &S) -> Vec<KeyPackage> {
    let mut packages = Vec::new();
    let (files, _) = match crate::utils::files::collect_ext_files(
        &path.as_ref().to_string_lossy(),
        false,
        &["exe"],
    ) {
        Ok(f) => f,
        Err(_) => {
            return packages;
        }
    };
    for file in files {
        match load_key_package_from_path(&file) {
            Ok(key) => packages.push(key),
            Err(_e) => {
                // println!("{}\n{}", e, e.backtrace());
            }
        }
    }
    packages
}

fn load_key_package_from_path<S: AsRef<std::path::Path> + ?Sized>(path: &S) -> Result<KeyPackage> {
    let view = pelite::FileMap::open(path)?;
    let mut last_error = match load_key_package(&view, path) {
        Ok(key) => return Ok(key),
        Err(e) => e,
    };
    if libsteamless::is_steamstub(&view) {
        let options = libsteamless::SteamlessOptions::default();
        match libsteamless::process_data(view.as_ref().to_vec(), &options, &|_level, _message| {}) {
            Ok(unpacked) => match load_key_package(&unpacked, path) {
                Ok(key) => return Ok(key),
                Err(e) => {
                    last_error = e;
                }
            },
            Err(err) => {
                last_error = err;
            }
        }
    }
    let v = view.as_ref();
    // DMM DRM may put game exe at the end of protected exe
    if let Some(pos) = memchr::memmem::find(&v[4..], &v[..4]) {
        let nv = &v[4 + pos..];
        match load_key_package(&nv, path) {
            Ok(key) => return Ok(key),
            Err(e) => {
                last_error = e;
            }
        }
    }
    Err(last_error)
}

fn find_resource<
    'a,
    'b,
    D: Into<pelite::resources::Name<'b>>,
    N: Into<pelite::resources::Name<'b>>,
>(
    resources: &pelite::resources::Resources<'a>,
    dir: D,
    path: N,
) -> Result<&'a [u8]> {
    use pelite::resources::*;
    let base = resources.root()?.get_dir(dir.into())?;
    let ent = base.get(path.into())?;
    match ent {
        Entry::DataEntry(data) => Ok(data.bytes()?),
        Entry::Directory(dir) => Ok(dir.first_data()?.bytes()?),
    }
}

/// Minimal PE section info for RVA → file offset mapping.
struct PeSections {
    image_base: u64,
    sections: Vec<(u32, u32, u32, u32)>, // (va, virtual_size, raw, raw_size)
}

fn parse_pe_sections(data: &[u8]) -> Result<PeSections> {
    if data.len() < 0x40 {
        anyhow::bail!("PE file too small");
    }
    let pe_off = u32::from_le_bytes(data[0x3C..0x40].try_into()?) as usize;
    if pe_off + 4 > data.len() || &data[pe_off..pe_off + 4] != b"PE\0\0" {
        anyhow::bail!("Not a PE file");
    }
    let coff = pe_off + 4;
    let section_count = u16::from_le_bytes(data[coff + 2..coff + 4].try_into()?) as usize;
    let optional_size = u16::from_le_bytes(data[coff + 16..coff + 18].try_into()?) as usize;
    let optional = coff + 20;
    let magic = u16::from_le_bytes(data[optional..optional + 2].try_into()?);
    let image_base = if magic == 0x10B {
        // PE32
        u32::from_le_bytes(data[optional + 28..optional + 32].try_into()?) as u64
    } else if magic == 0x20B {
        // PE32+
        u64::from_le_bytes(data[optional + 24..optional + 32].try_into()?)
    } else {
        anyhow::bail!("Unsupported PE optional header magic 0x{magic:x}");
    };
    let section_table = optional + optional_size;
    let mut sections = Vec::with_capacity(section_count);
    for i in 0..section_count {
        let off = section_table + i * 40;
        let va = u32::from_le_bytes(data[off + 12..off + 16].try_into()?);
        let virtual_size = u32::from_le_bytes(data[off + 8..off + 12].try_into()?);
        let raw_size = u32::from_le_bytes(data[off + 16..off + 20].try_into()?);
        let raw = u32::from_le_bytes(data[off + 20..off + 24].try_into()?);
        let size = virtual_size.max(raw_size);
        sections.push((va, size, raw, raw_size));
    }
    Ok(PeSections {
        image_base,
        sections,
    })
}

fn rva_to_offset(ps: &PeSections, rva: u32) -> Option<u32> {
    for &(va, size, raw, raw_size) in &ps.sections {
        if va <= rva && rva < va + size {
            let offset = raw + (rva - va);
            if offset < raw + raw_size {
                return Some(offset);
            }
        }
    }
    None
}

fn va_to_rva(ps: &PeSections, va: u64) -> Option<u32> {
    if va >= ps.image_base {
        Some((va - ps.image_base) as u32)
    } else {
        None
    }
}

const BRES_SALT_SIZE: usize = 0x2000;

/// Strategy 1: scan x86 code for the adjacent pair
///   mov dword ptr [salt_ptr_global], offset salt_bytes
///   mov dword ptr [salt_size_global], 2000h
fn iter_salt_assignment_candidates(data: &[u8], ps: &PeSections) -> Vec<(u32, Vec<u8>)> {
    let mut candidates = Vec::new();
    if data.len() < 20 {
        return candidates;
    }
    let mut off = 0usize;
    while off + 20 <= data.len() {
        if &data[off..off + 2] != b"\xC7\x05" {
            off += 1;
            continue;
        }
        if off + 10 > data.len() {
            break;
        }
        let salt_va = u32::from_le_bytes(data[off + 6..off + 10].try_into().unwrap());
        if (salt_va as u64) < ps.image_base {
            off += 1;
            continue;
        }
        let salt_rva = match va_to_rva(ps, salt_va as u64) {
            Some(rva) => rva,
            None => {
                off += 1;
                continue;
            }
        };
        let salt_file_off = match rva_to_offset(ps, salt_rva) {
            Some(o) => o as usize,
            None => {
                off += 1;
                continue;
            }
        };
        if salt_file_off + BRES_SALT_SIZE > data.len() {
            off += 1;
            continue;
        }
        // Look for the size assignment (0x2000) nearby
        let window_end = (off + 64).min(data.len().saturating_sub(10));
        for size_off in (off + 10..window_end).step_by(1) {
            if &data[size_off..size_off + 2] != b"\xC7\x05" {
                continue;
            }
            let size_val =
                u32::from_le_bytes(data[size_off + 6..size_off + 10].try_into().unwrap());
            if size_val != BRES_SALT_SIZE as u32 {
                continue;
            }
            let salt = data[salt_file_off..salt_file_off + BRES_SALT_SIZE].to_vec();
            candidates.push((salt_file_off as u32, salt));
            break;
        }
        off += 1;
    }
    candidates
}

/// Strategy 2: for packed EXEs, locate salt from data markers.
fn iter_packed_neighborhood_candidates(data: &[u8]) -> Vec<(u32, Vec<u8>)> {
    let mut candidates = Vec::new();
    let mut seen = std::collections::HashSet::new();

    // Marker "V2Link\0\0" — salt sits immediately before it
    let marker_v2 = b"V2Link\x00\x00";
    let mut cursor = 0;
    while let Some(pos) = data[cursor..]
        .windows(marker_v2.len())
        .position(|w| w == marker_v2)
    {
        let anchor = cursor + pos;
        if anchor >= BRES_SALT_SIZE {
            let salt_off = anchor - BRES_SALT_SIZE;
            if salt_off + BRES_SALT_SIZE <= data.len() && seen.insert(salt_off) {
                candidates.push((
                    salt_off as u32,
                    data[salt_off..salt_off + BRES_SALT_SIZE].to_vec(),
                ));
            }
        }
        cursor = anchor + 1;
    }

    // Marker "forcedataxp3\0" — salt starts at 16-byte alignment after it
    let marker_fp3 = b"forcedataxp3\x00";
    cursor = 0;
    while let Some(pos) = data[cursor..]
        .windows(marker_fp3.len())
        .position(|w| w == marker_fp3)
    {
        let anchor = cursor + pos;
        let window_start = (anchor + marker_fp3.len() + 0xF) & !0xF;
        let window_end = (anchor + 0x100).min(data.len().saturating_sub(BRES_SALT_SIZE));
        for salt_off in (window_start..=window_end).step_by(0x10) {
            if salt_off + BRES_SALT_SIZE <= data.len() && seen.insert(salt_off) {
                candidates.push((
                    salt_off as u32,
                    data[salt_off..salt_off + BRES_SALT_SIZE].to_vec(),
                ));
            }
        }
        cursor = anchor + 1;
    }

    candidates
}

/// Locate the 8192‑byte bres salt embedded in a PE executable.
///
/// Mirrors the Python `iter_auto_salt_candidates` + `load_salt` logic from
/// the cxdec‑hxv4‑static‑analysis toolchain.
fn load_bres_salt<S: AsRef<[u8]> + ?Sized>(data: &S) -> Result<Vec<u8>> {
    let data = data.as_ref();
    let ps = parse_pe_sections(data)?;

    // Strategy 1: code assignments
    let candidates = iter_salt_assignment_candidates(data, &ps);
    if !candidates.is_empty() {
        // println!(
        //     "Located bres salt via code assignment (file offset 0x{:x}).",
        //     candidates[0].0
        // );
        return Ok(candidates[0].1.clone());
    }

    // Strategy 2: packed-neighborhood markers
    let candidates = iter_packed_neighborhood_candidates(data);
    if !candidates.is_empty() {
        // println!(
        //     "Located bres salt via data marker (file offset 0x{:x}).",
        //     candidates[0].0
        // );
        return Ok(candidates[0].1.clone());
    }

    anyhow::bail!("Could not locate 0x{BRES_SALT_SIZE:x}-byte bres salt in PE");
}

fn decode_bres_root(text: &[u8]) -> Result<String> {
    let text = decode_to_string(Encoding::Utf16LE, text, true)?;
    let text = text.trim_end_matches('\0');
    if !text.starts_with("bres://./") {
        anyhow::bail!("Unexpected bres root: {text}");
    }
    Ok(text[9..].trim_end_matches('/').to_owned())
}

fn decrypt_bres(data: &[u8], path_key: &str, salt: &[u8]) -> Result<Vec<u8>> {
    // generate chacha8 params
    use chacha20::ChaCha8;
    use chacha20::cipher::{StreamCipher, StreamCipherSeek};
    use sha3::{Digest, Sha3_384};
    let mut h = Sha3_384::new();
    h.update(encode_string(Encoding::Utf16LE, path_key, true)?);
    h.update(salt);
    let mut digest = MemReader::new(h.finalize().to_vec());
    let mut key_bytes = [0u8; 32];
    let mut nonce = [0; 2];
    digest.read_exact(&mut key_bytes)?;
    for k in nonce.iter_mut() {
        *k = digest.read_u32()?;
    }
    let ctr_base = digest.read_u32()?;
    let ctr_high = digest.read_u32()?;
    let mut data = data.to_vec();
    let mut nonce_bytes = [0u8; 12];
    nonce_bytes[0..4].copy_from_slice(&ctr_high.to_le_bytes());
    for (i, &word) in nonce.iter().enumerate() {
        nonce_bytes[4 + i * 4..4 + (i + 1) * 4].copy_from_slice(&word.to_le_bytes());
    }
    let mut cipher = ChaCha8::new_from_slices(&key_bytes, &nonce_bytes)?;
    let chunk_size = 64;
    for (bn, chunk) in data.chunks_mut(chunk_size).enumerate() {
        let ctr_low = ctr_base ^ (bn as u32);
        cipher.seek((ctr_low as u64) * 64);
        cipher.apply_keystream(chunk);
    }
    Ok(data)
}

fn parse_tjs_strings(data: &[u8]) -> Result<Vec<String>> {
    use super::super::super::super::tjs2::*;
    if !data.starts_with(b"TJS2100\0") {
        anyhow::bail!("invalid tjs2 compiled script.");
    }
    let mut reader = MemReaderRef::new(data);
    reader.pos = 0xC;
    let data = DataArea::unpack(&mut reader, false, Encoding::Utf16LE, &None)?;
    Ok(data.string_array)
}

fn find_bootstrap_url<'a>(strings: &'a Vec<String>) -> Result<&'a str> {
    for value in strings.iter() {
        if value.starts_with("bres://./") && value.to_lowercase().ends_with("/bootstrap") {
            return Ok(value.as_str());
        }
    }
    anyhow::bail!("Could not find bootstrap bres URL in STARTUP.TJS strings")
}

fn bres_key_from_url(url: &str) -> Result<String> {
    if !url.starts_with("bres://./") {
        anyhow::bail!("Not a local bres URL: {url}");
    }
    Ok(url[9..]
        .split('/')
        .next()
        .ok_or_else(|| anyhow::anyhow!("No data"))?
        .to_owned())
}

const PARAMS_PAT: &[u8] = b"\0\0\0\0\0\0\0\0PARAMS";
const UPKEY_PAT: &[u8] = b"p\0t\0-\0-\0n\0o\0\0\0\0\0";

fn parse_config_table(dll: &[u8]) -> Result<HashMap<String, Vec<u8>>> {
    let mut ofs =
        memchr::memmem::find(dll, PARAMS_PAT).ok_or_else(|| anyhow::anyhow!("No parmas in dll"))?;
    ofs += PARAMS_PAT.len() - 6;
    let mut reader = MemReaderRef::new(dll);
    reader.pos = ofs;
    let mut tags = HashMap::new();
    loop {
        let tag = reader.read_cstring()?;
        if tag.as_bytes().is_empty() {
            break;
        }
        let tag = decode_to_string(Encoding::Utf8, tag.as_bytes(), true)?;
        let length = reader.read_u16()?;
        let value = reader.read_exact_vec(length as usize)?;
        tags.insert(tag, value);
    }
    if let Some(mut ofs) = memchr::memmem::find(dll, UPKEY_PAT) {
        ofs += UPKEY_PAT.len();
        reader.pos = ofs;
        tags.insert("upperKey".into(), reader.read_exact_vec(8)?);
    }
    Ok(tags)
}

struct TjsVM<'a> {
    file: &'a Tjs2File,
    obj: &'a Tjs2Object,
    i: usize,
    reg: HashMap<i32, String>,
}

impl<'a> TjsVM<'a> {
    fn new(file: &'a Tjs2File, obj: &'a Tjs2Object) -> Self {
        Self {
            file,
            obj,
            i: 0,
            reg: HashMap::new(),
        }
    }
    fn run(&mut self) -> Result<String> {
        let len = self.obj.code.len();
        while self.i < len {
            if let Some(s) = self.run_step()? {
                return Ok(s);
            }
        }
        anyhow::bail!("No _bootStrap calld call invoked.");
    }
    fn run_step(&mut self) -> Result<Option<String>> {
        use tjs2dec::Variant;
        use tjs2dec::vmcodes::vm::*;
        let code = &self.obj.code;
        let op = code[self.i];
        const VM_INC1: i32 = VM_INC + 1;
        const VM_INC2: i32 = VM_INC + 2;
        const VM_INC3: i32 = VM_INC + 3;
        const VM_DEC1: i32 = VM_DEC + 1;
        const VM_DEC2: i32 = VM_DEC + 2;
        const VM_DEC3: i32 = VM_DEC + 3;
        match op {
            VM_CONST => {
                self.ensure(3)?;
                let dst_reg = code[self.i + 1];
                let src = code[self.i + 2];
                if let Some(v) = self.obj.data.get(src as usize) {
                    match v {
                        Variant::String(idx) => {
                            if let Some(s) = self.file.const_pools.strings.get(*idx as usize) {
                                self.reg.insert(dst_reg, s.to_owned());
                            }
                        }
                        _ => {}
                    }
                }
                self.i += 3;
            }
            VM_DELD | VM_TYPEOFD | VM_DELI | VM_TYPEOFI | VM_GPD | VM_GPDS | VM_SPD | VM_SPDE
            | VM_SPDEH | VM_SPDS | VM_GPI | VM_GPIS | VM_SPI | VM_SPIE | VM_SPIS | VM_INC1
            | VM_INC2 | VM_DEC1 | VM_DEC2 => {
                self.skip(4)?;
            }
            VM_CP | VM_CEQ | VM_CDEQ | VM_CLT | VM_CGT | VM_CHKINS | VM_ADDCI | VM_CHGTHIS
            | VM_CCL | VM_ENTRY | VM_SETP | VM_GETP | VM_INC3 | VM_DEC3 => {
                self.skip(3)?;
            }
            VM_CL | VM_SRV | VM_GLOBAL | VM_THROW | VM_TT | VM_TF | VM_SETF | VM_SETNF
            | VM_LNOT | VM_BNOT | VM_ASC | VM_CHR | VM_NUM | VM_CHS | VM_INV | VM_CHKINV
            | VM_TYPEOF | VM_EVAL | VM_EEXP | VM_INT | VM_REAL | VM_STR | VM_OCTET | VM_JF
            | VM_JNF | VM_JMP | VM_INC | VM_DEC => {
                self.skip(2)?;
            }
            VM_RET | VM_NOP | VM_NF | VM_EXTRY | VM_REGMEMBER | VM_DEBUGGER => {
                self.skip(1)?;
            }
            VM_CALL | VM_CALLD | VM_CALLI | VM_NEW => {
                let ns = match op {
                    VM_CALL | VM_NEW => 4,
                    VM_CALLD | VM_CALLI => 5,
                    _ => unreachable!(),
                };
                self.ensure(ns)?;
                let i = self.i;
                let argc = code[i + ns - 1];
                self.i += ns;
                if argc == -1 {
                    // omit args
                } else if argc == -2 {
                    // expand args
                    self.ensure(1)?;
                    let num = code[i + ns] as usize;
                    self.i += 1;
                    self.ensure(num * 2)?;
                    self.i += num * 2;
                } else {
                    let argc = argc as usize;
                    self.ensure(argc)?;
                    if op == VM_CALLD {
                        let obj = code[i + 2];
                        let member = code[i + 3];
                        // %proxy
                        if obj == -2 {
                            if let Some(v) = self.obj.data.get(member as usize) {
                                match v {
                                    Variant::String(idx) => {
                                        if let Some(s) =
                                            self.file.const_pools.strings.get(*idx as usize)
                                        {
                                            if s == "_bootStrap" {
                                                if argc >= 1 {
                                                    let farg = code[i + 5];
                                                    return Ok(Some(
                                                        self.reg
                                                            .get(&farg)
                                                            .ok_or_else(|| {
                                                                anyhow::anyhow!(
                                                                    "No string in %{}",
                                                                    farg
                                                                )
                                                            })?
                                                            .to_owned(),
                                                    ));
                                                }
                                            }
                                        }
                                    }
                                    _ => {}
                                }
                            }
                        }
                    }
                    self.i += argc;
                }
            }
            _ => {
                for base in [
                    VM_LOR, VM_LAND, VM_BOR, VM_BXOR, VM_BAND, VM_SAR, VM_SAL, VM_SR, VM_ADD,
                    VM_SUB, VM_MOD, VM_DIV, VM_IDIV, VM_MUL,
                ] {
                    match op - base {
                        0 => {
                            self.skip(3)?;
                            return Ok(None);
                        }
                        1 | 2 => {
                            self.skip(5)?;
                            return Ok(None);
                        }
                        3 => {
                            self.skip(4)?;
                            return Ok(None);
                        }
                        _ => {}
                    }
                }
                anyhow::bail!("unknown instruction {}", op);
            }
        }
        Ok(None)
    }

    fn ensure(&self, need: usize) -> Result<()> {
        if self.i + need > self.obj.code.len() {
            anyhow::bail!(
                "truncated instruction at {}: need {}, code_len {}",
                self.i,
                need,
                self.obj.code.len()
            );
        }
        Ok(())
    }

    fn skip(&mut self, size: usize) -> Result<()> {
        self.ensure(size)?;
        self.i += size;
        Ok(())
    }
}

fn get_boot_strap(tjs: &[u8]) -> Result<String> {
    use tjs2dec::load_tjs2_bytecode;
    let file = load_tjs2_bytecode(tjs)?;
    let global_obj = file
        .objects
        .iter()
        .find(|s| s.name.as_ref().is_some_and(|s| s == "global"))
        .ok_or_else(|| anyhow::anyhow!("No global object."))?;
    let mut vm = TjsVM::new(&file, global_obj);
    vm.run()
}

fn load_key_package<S: AsRef<[u8]> + ?Sized, P: AsRef<std::path::Path> + ?Sized>(
    data: &S,
    path: &P,
) -> Result<KeyPackage> {
    let bn = path
        .as_ref()
        .file_stem()
        .map(|s| (&s.to_string_lossy()).to_string())
        .unwrap_or_else(|| "TBD".into());
    let file = PeFile::from_bytes(data)?;
    let resources = file.resources()?;
    let bootstrap = find_resource(&resources, 10, "BOOTSTRAP")?;
    let startup_tjs = find_resource(&resources, 10, "STARTUP.TJS")?;
    let text = find_resource(&resources, "TEXT", 127)?;
    // println!("Resource loaded.");
    let salt = load_bres_salt(data)?;
    // println!(
    //     "bres_salt={}b",
    //     hex::encode(salt.get(..16).unwrap_or_default())
    // );
    let text = decode_bres_root(text)?;
    // println!("bres root: {text}");
    let startup_tjs = decrypt_bres(startup_tjs, &text, &salt)?;
    let strings = parse_tjs_strings(&startup_tjs)?;
    // println!("strings: {strings:#?}");
    let bootstrap_url = find_bootstrap_url(&strings)?;
    // println!("bootstrap url: {bootstrap_url}");
    let bootstrap_key = bres_key_from_url(bootstrap_url)?;
    // println!("bootstrap key: {bootstrap_key}");
    let bootstrap = decrypt_bres(bootstrap, &bootstrap_key, &salt)?;
    // println!(
    //     "bootstrap={}b",
    //     hex::encode(bootstrap.get(..16).unwrap_or_default())
    // );
    let mut reader = flate2::read::ZlibDecoder::new(MemReaderRef::new(&bootstrap[8..]));
    let mut dll = Vec::new();
    reader.read_to_end(&mut dll)?;
    if !dll.starts_with(b"MZ") {
        anyhow::bail!("Not a dll.");
    }
    let config = parse_config_table(&dll)?;
    let warning = config
        .get("WARNING")
        .ok_or_else(|| anyhow::anyhow!("WARNING not found"))?;
    let warning = decode_to_string(Encoding::Utf8, &warning, true)?;
    // println!("warning: {warning}");
    let params = config
        .get("PARAMS")
        .cloned()
        .ok_or_else(|| anyhow::anyhow!("PARAMS not found"))?;
    // println!("params: {}", hex::encode(&params));
    let archive_unique_key = config
        .get("UNIQUE")
        .ok_or_else(|| anyhow::anyhow!("UNIQUE not found"))?;
    let archive_unique_key = decode_to_string(Encoding::Utf16LE, &archive_unique_key, true)?;
    // println!("archive_unique_key: {archive_unique_key}");
    let upper_key = config.get("upperKey").cloned();
    // println!(
    //     "upper_key: {:?}",
    //     upper_key.as_ref().map(|s| hex::encode(s))
    // );
    let boot_strap = get_boot_strap(&startup_tjs)?;
    // println!("boot_strap: {boot_strap:?}");
    Ok(KeyPackage {
        description: "TBD".into(),
        key: KeyData {
            boot_strap,
            warning,
            params,
            archive_unique_key,
            upper_key,
        },
        sku: bn,
    })
}

#[test]
fn test_triple32() {
    assert_eq!(triple32(0x281ff4b9), 0x3389ba89);
    assert_eq!(triple32(0x4899abb), 0xca5cb43b);
    assert_eq!(triple32(0x12fb3c7b), 0xe2855413);
    assert_eq!(triple32(0x275bdef0), 0x8f95ac52);
    assert_eq!(triple32(0x4e9302ee), 0x7b62fdd0);
    assert_eq!(triple32(0x4df4823b), 0xe7483578);
    assert_eq!(triple32(0x77b0cd89), 0x7d42d107);
    assert_eq!(triple32(0x312bebee), 0xa038d73f);
    assert_eq!(triple32(0x39203931), 0xf7b87c25);
    assert_eq!(triple32(0x633b66f4), 0x98ff988);
    assert_eq!(triple32(0x636d1fc1), 0x99897c6);
    assert_eq!(triple32(0xb7a114f), 0xef0b8bd3);
    assert_eq!(triple32(0x4c7d96c0), 0xc1ba0efe);
    assert_eq!(triple32(0x7f26226e), 0x7449b080);
    assert_eq!(triple32(0x1bd4bcc7), 0xea9264aa);
    assert_eq!(triple32(0x13afe6fd), 0x66396c69);
}

#[test]
fn test_gen_keys() {
    let keys = hxkeys_new(
        "BOOTSTRAPbootstrap0123456789",
        "WARNINGwarning0123456789",
        b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a",
        "ArchiveUniqueKey0123456789",
        Some(*b"\x00\x11\x22\x33\x44\x55\x66\x77"),
    )
    .unwrap();
    let expected_key: [u8; 32] =
        hex::decode("4fb07f17eb1d7d0f14fba645e067d5d90a973494f4161da962ee49ccfc9ad237")
            .unwrap()
            .try_into()
            .unwrap();
    let expected_nonce_a: [u8; 24] =
        hex::decode("c84f47adef9093396d421105bd8893c925b3853aef22d346")
            .unwrap()
            .try_into()
            .unwrap();
    let expected_nonce_b: [u8; 24] =
        hex::decode("98d9fc0c47eb2684aad17ca33ee8cb1aed30812ee8990500")
            .unwrap()
            .try_into()
            .unwrap();
    assert_eq!(keys.0.key, expected_key);
    assert_eq!(&keys.0.nonce_a[..24], &expected_nonce_a);
    assert_eq!(&keys.0.nonce_b[..24], &expected_nonce_b);
}

#[test]
fn test_real_keys() {
    let (key, mut params) = hxkeys_new(
        "LimeLightRemonadeJam (C)YUZUSOFT/JUNOS INC. All Rights Reserved.",
        "Warning! Extracting this game data may infringe on author's rights.",
        b"\x02\x00\x06\x05\x01\x04\x03\x07\x01\x05\x03\x02\x00\x04\x01\x00\x02\x01\xe2\x02\x83\x02",
        "{EnaAnjTukRirMikNay}",
        Some(*b"\xbf\x22\x36\x8a\x48\x21\x02\x06"),
    )
    .unwrap();
    map_key_to_garbro(&mut params);
    assert_eq!(params.mask, 738);
    assert_eq!(params.offset, 643);
    use base64::Engine;
    let b64 = base64::engine::general_purpose::STANDARD;
    assert_eq!(b64.encode(params.prologue_perm), "AQAC");
    assert_eq!(b64.encode(params.odd_branch_perm), "AQIEAwAF");
    assert_eq!(b64.encode(params.even_branch_perm), "AgUABwYBAwQ=");
    assert_eq!(u64::from_le_bytes(key.filter), 13089994567570788352);
    let (ind1, ind2) = gen_index_keys(&key).unwrap();
    assert_eq!(
        b64.encode(&ind1.key),
        "fMktWafCUSPGVDvR/8LUx9f+yh3Y+PIq90XmzJ6xZhI="
    );
    assert_eq!(b64.encode(&ind1.nonce), "aDnPYFowPzhVdOsfJweaYA==");
    assert_eq!(
        b64.encode(&ind2.key),
        "kHMdDweFjeDFVTwQA10XQAPUAOfXzKp2ukygeLzGzHg="
    );
    assert_eq!(b64.encode(&ind2.nonce), "CSBjzSXQNTioPhDp710WCQ==");
    assert!((params.flags & RANDOM_TYPE_FLAG) == 0);
}

#[test]
fn test_filehash_deserialize() {
    assert_eq!(
        FileHash([
            0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x0,
            0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf
        ]),
        serde_json::from_str(
            "\"000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0F\""
        )
        .unwrap()
    );
}

#[test]
fn test_calculate_path_hash() {
    assert_eq!(
        calculate_path_hash("", "xp3hnp"),
        PathHash::try_from("94d4a97c61498621").unwrap(),
    );
    assert_eq!(
        calculate_path_hash("scenario/scripts/", "xp3hnp"),
        PathHash::try_from("c81c19411c1a5e54").unwrap(),
    );
}
