Skip to content

Commit

Permalink
Merge pull request #47 from tremwil/feat/zstd-dcx
Browse files Browse the repository at this point in the history
Feature: ZSTD support for DCX
garyttierney authored Aug 16, 2024
2 parents fa3b8e3 + 7bfc055 commit 1b694fa
Showing 9 changed files with 148 additions and 9 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -3,3 +3,4 @@
/extract
/firedbg
/.idea
/.vscode
50 changes: 50 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/formats/Cargo.toml
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ rsa = "0.9"
thiserror.workspace = true
widestring = "1"
zerocopy = { version = "0.7.32", features = ["derive"] }
zstd = "0.13"

[lints]
workspace = true
10 changes: 9 additions & 1 deletion crates/formats/src/dcx/mod.rs
Original file line number Diff line number Diff line change
@@ -7,15 +7,18 @@ use std::{
use byteorder::BE;
use thiserror::Error;
use zerocopy::{FromBytes, FromZeroes, U32};
use zstd::ZstdDecoder;

use self::{deflate::DeflateDecoder, oodle::OodleReader};

pub mod deflate;
pub mod oodle;
pub mod zstd;

const MAGIC_DCX: u32 = 0x44435800;
const MAGIC_ALGORITHM_KRAKEN: &[u8; 4] = b"KRAK";
const MAGIC_ALGORITHM_DEFLATE: &[u8; 4] = b"DFLT";
const MAGIC_ALGORITHM_ZSTD: &[u8; 4] = b"ZSTD";

#[derive(Debug, Error)]
pub enum DcxError {
@@ -69,6 +72,9 @@ impl DcxHeader {
.ok_or(DcxError::DecoderError)?,
),
MAGIC_ALGORITHM_DEFLATE => Decoder::Deflate(DeflateDecoder::new(reader)),
MAGIC_ALGORITHM_ZSTD => {
Decoder::Zstd(ZstdDecoder::new(reader).map_err(|_| DcxError::DecoderError)?)
}
_ => return Err(DcxError::UnknownAlgorithm(algorithm.to_owned())),
};

@@ -111,6 +117,7 @@ impl Debug for DcxHeader {
pub enum Decoder<R: Read> {
Kraken(OodleReader<R>),
Deflate(DeflateDecoder<R>),
Zstd(ZstdDecoder<R>),
}

pub struct DcxContentDecoder<R: Read> {
@@ -131,6 +138,7 @@ impl<R: Read> Read for DcxContentDecoder<R> {
match &mut self.decoder {
Decoder::Kraken(d) => d.read(buf),
Decoder::Deflate(d) => d.read(buf),
Decoder::Zstd(d) => d.read(buf),
}
}
}
@@ -196,7 +204,7 @@ impl Debug for Sizes {
pub struct CompressionParameters {
chunk_magic: [u8; 4],

/// Either KRAK, DFLT or EDGE
/// Either KRAK, DFLT, EDGE or ZSTD
algorithm: [u8; 4],

/// Seems to the size of the current DCP chunk including magic and algo
4 changes: 2 additions & 2 deletions crates/formats/src/dcx/oodle.rs
Original file line number Diff line number Diff line change
@@ -6,10 +6,10 @@ use std::{
use fstools_oodle_rt::{decoder::OodleDecoder, Compressor, Oodle, OODLELZ_BLOCK_LEN};

// SAFETY: `OodleLZDecoder` pointer is safe to use across several threads.
unsafe impl<R: Read> Sync for OodleReader<R> {}
unsafe impl<R: Read + Sync> Sync for OodleReader<R> {}

// SAFETY: See above.
unsafe impl<R: Read> Send for OodleReader<R> {}
unsafe impl<R: Read + Send> Send for OodleReader<R> {}

pub struct OodleReader<R: Read> {
reader: R,
16 changes: 16 additions & 0 deletions crates/formats/src/dcx/zstd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use std::io::{self, Read};

/// Trivial wrapper around a [`zstd::Decoder<BufReader<R>>`].
pub struct ZstdDecoder<R: Read>(zstd::Decoder<'static, io::BufReader<R>>);

impl<R: Read> ZstdDecoder<R> {
pub fn new(reader: R) -> io::Result<Self> {
Ok(Self(zstd::Decoder::new(reader)?))
}
}

impl<R: Read> Read for ZstdDecoder<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
}
4 changes: 3 additions & 1 deletion crates/support/elden_ring/Cargo.toml
Original file line number Diff line number Diff line change
@@ -7,4 +7,6 @@ repository.workspace = true
authors.workspace = true

[dependencies]
fstools.workspace = true
fstools.workspace = true
aes = "0.8"
cbc = "0.1"
49 changes: 47 additions & 2 deletions crates/support/elden_ring/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
use std::{io, path::PathBuf};
use std::{
io::{self, Read},
path::{Path, PathBuf},
};

use fstools::dvdbnd::{ArchiveKeyProvider, DvdBnd};
use aes::cipher::{BlockDecryptMut, KeyIvInit};
use fstools::{
dvdbnd::{ArchiveKeyProvider, DvdBnd},
formats::{bnd4::BND4, dcx::DcxHeader},
};

pub fn load_dvd_bnd(
game_path: PathBuf,
@@ -23,3 +30,41 @@ pub fn dictionary() -> impl Iterator<Item = PathBuf> {
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(std::path::PathBuf::from)
}

pub fn decrypt_regulation(reader: &mut impl Read) -> io::Result<Vec<u8>> {
const REGULATION_KEY: &'static [u8; 32] = &[
0x99, 0xBF, 0xFC, 0x36, 0x6A, 0x6B, 0xC8, 0xC6, 0xF5, 0x82, 0x7D, 0x09, 0x36, 0x02, 0xD6,
0x76, 0xC4, 0x28, 0x92, 0xA0, 0x1C, 0x20, 0x7F, 0xB0, 0x24, 0xD3, 0xAF, 0x4E, 0x49, 0x3F,
0xEF, 0x99,
];

let mut iv = [0u8; 16];
reader.read_exact(&mut iv)?;

let mut out_buf = Vec::new();
reader.read_to_end(&mut out_buf)?;

type Aes256Cbc = cbc::Decryptor<aes::Aes256>;
let mut cipher = Aes256Cbc::new_from_slices(REGULATION_KEY, &iv).unwrap();

// SAFETY: GenericArray<u8, _> is safe to transmute from an equiv. slice of u8s
unsafe {
cipher.decrypt_blocks_mut(out_buf.align_to_mut().1);
}

Ok(out_buf)
}

pub fn load_regulation(game_path: impl AsRef<Path>) -> io::Result<BND4> {
let regulation_bytes = std::fs::read(game_path.as_ref().join("regulation.bin"))?;
let dcx_bytes = decrypt_regulation(&mut regulation_bytes.as_slice())?;

let (_, mut dcx_decoder) = DcxHeader::read(io::Cursor::new(dcx_bytes))
.map_err(|_| io::Error::other("DCX header reading failed"))?;

let mut bnd4_bytes = Vec::new();
dcx_decoder.read_to_end(&mut bnd4_bytes)?;

BND4::from_reader(io::Cursor::new(bnd4_bytes))
.map_err(|_| io::Error::other("Failed to read regulation BND4"))
}
22 changes: 19 additions & 3 deletions tests/dcx.rs
Original file line number Diff line number Diff line change
@@ -2,19 +2,21 @@ use std::{
collections::HashSet,
error::Error,
ffi::OsStr,
io::{self, Read},
path::{Path, PathBuf},
sync::Arc,
};

use fstools::{formats::dcx::DcxHeader, prelude::*};
use fstools_elden_ring_support::dictionary;
use fstools_elden_ring_support::{decrypt_regulation, dictionary};
use fstools_formats::dcx::DcxError;
use insta::assert_snapshot;
use libtest_mimic::{Arguments, Failed, Trial};

fn main() -> Result<(), Box<dyn Error>> {
let args = Arguments::from_args();
let er_path = PathBuf::from(std::env::var("ER_PATH").expect("er_path"));
let reg_path = er_path.join("regulation.bin");
let keys_path = PathBuf::from(std::env::var("ER_KEYS_PATH").expect("er_keys_path"));
let vfs = Arc::new(fstools_elden_ring_support::load_dvd_bnd(
er_path,
@@ -39,24 +41,38 @@ fn main() -> Result<(), Box<dyn Error>> {
tests.push(test);
}

// Test against the regulation DCX (ZSTD encoded since 1.12)
tests.push(Trial::test("regulation.bin", move || {
check_regulation(&reg_path)
}));

libtest_mimic::run(&args, tests).exit();
}

pub fn check_regulation(path: &Path) -> Result<(), Failed> {
let regulation_bytes = std::fs::read(path.join("regulation.bin"))?;
let dcx_bytes = decrypt_regulation(&mut regulation_bytes.as_slice())?;
check_dcx(io::Cursor::new(dcx_bytes))
}

pub fn check_file(vfs: Arc<DvdBnd>, file: &Path) -> Result<(), Failed> {
let file = match vfs.open(file.to_string_lossy().as_ref()) {
Ok(file) => file,
Err(_) => {
return Ok(());
}
};
check_dcx(file)
}

let (_, mut reader) = match DcxHeader::read(file) {
pub fn check_dcx(reader: impl Read) -> Result<(), Failed> {
let (_, mut decoder) = match DcxHeader::read(reader) {
Ok(details) => details,
Err(DcxError::UnknownAlgorithm(_)) => return Ok(()),
Err(_) => return Err("failed to parse DCX header".into()),
};

std::io::copy(&mut reader, &mut std::io::sink())?;
std::io::copy(&mut decoder, &mut std::io::sink())?;

Ok(())
}

0 comments on commit 1b694fa

Please sign in to comment.