Encoding File Format
The encoding file is the gateway to all CASC content. It maps content keys (unencoded file hashes) to encoding keys (encoded/compressed file hashes) and provides essential metadata for content resolution.
Overview
The encoding file serves multiple critical functions:
- Content Resolution: Maps content keys to encoding keys for CDN retrieval
- Compression Metadata: Specifies ESpec encoding for each file
- Size Information: Tracks both compressed and decompressed sizes
- Multi-Version Support: Handles multiple encoding keys per content key
File Structure
The encoding file is BLTE-encoded and consists of:
[BLTE Container]
[Header] (22 bytes)
[ESpec Table] (variable)
[CKey Page Index] (variable)
[CKey Pages] (variable)
[EKey Page Index] (variable)
[EKey Pages] (variable)
[File ESpec] (variable) - The encoding file's own ESpec
Binary Format
Header (22 bytes)
struct EncodingHeader {
uint16_t magic; // 0x00: 'EN' (0x454E)
uint8_t version; // 0x02: Version (1)
uint8_t ckey_size; // 0x03: Content key size (16)
uint8_t ekey_size; // 0x04: Encoding key size (16)
uint16_t ckey_page_size; // 0x05: CKey page size in KB (BE)
uint16_t ekey_page_size; // 0x07: EKey page size in KB (BE)
uint32_t ckey_page_count; // 0x09: Number of CKey pages (BE)
uint32_t ekey_page_count; // 0x0D: Number of EKey pages (BE)
uint8_t flags; // 0x11: Flags (must be 0)
uint32_t espec_size; // 0x12: ESpec table size (BE)
};
ESpec String Table
Immediately follows the header. Contains null-terminated strings referenced by entries:
"z\0b:{0,4}\0b:{0,4},z\0b:{0,2},z:{0,6}\0...\0"
Common ESpec patterns:
-
z- ZLib compression -
n- No compression -
b:{start,size}- Block encoding (see ESpec) -
Empty string for uncompressed files
Page Index Tables
CKey Page Index
For each CKey page:
struct PageIndex {
uint8_t first_key[ckey_size]; // First key in the page
uint8_t page_hash[16]; // MD5 of the page data
};
EKey Page Index
Similar structure but uses ekey_size for the first key.
Content Key (CKey) Pages
Pages are sorted by content key for binary search. Each page contains multiple entries:
struct CKeyEntry {
uint8_t ekey_count; // Number of encoding keys
uint8_t file_size[5]; // Decompressed size (40-bit BE)
uint8_t ckey[ckey_size]; // Content key
uint8_t ekeys[ekey_size * ekey_count]; // Encoding keys
};
Entry layout (sizes from header):
[count:1] [size:5] [ckey:ckey_size] [ekey1:ekey_size] [ekey2:ekey_size] ...
Multiple EKeys: A single content key can map to multiple encoding keys, allowing:
-
Different compression algorithms for the same content
-
Regional variations with different encryption
-
Platform-specific optimizations
Encoding Key (EKey) Pages
Maps encoding keys to ESpec entries:
struct EKeyEntry {
uint8_t ekey[ekey_size]; // Encoding key
uint32_t espec_index; // Index into ESpec table (BE)
uint8_t file_size[5]; // Encoded file size (40-bit BE)
};
Padding Detection: EKey pages may contain padding entries that must be skipped. Two sentinel patterns indicate padding:
espec_index == 0xFFFFFFFF(Agent.exe sentinel)espec_index == 0with all key bytes0x00(zero-fill padding)
Content Resolution Process
-
Find CKey Entry:
- Binary search CKey page index for target page
- Linear search within page for content key
- Extract encoding key(s) and decompressed size
-
Find EKey Entry (optional):
- Binary search EKey page index
- Locate entry to get ESpec index and compressed size
-
Parse ESpec:
- Index into ESpec string table
- Parse encoding specification for compression details
Usage
Parsing
#![allow(unused)]
fn main() {
use cascette_formats::encoding::EncodingFile;
// From decompressed data
let encoding = EncodingFile::parse(&data)?;
// From BLTE-encoded CDN data
let encoding = EncodingFile::parse_blte(&blte_data)?;
}
Content Key Lookup
#![allow(unused)]
fn main() {
use cascette_crypto::ContentKey;
// Single lookup (binary search on page index, linear within page)
if let Some(ekey) = encoding.find_encoding(&content_key) {
println!("Encoding key: {:?}", ekey);
}
// Get all encoding keys for a content key
let ekeys = encoding.find_all_encodings(&content_key);
// Batch lookup (sort-merge across pages)
let results = encoding.batch_find_encodings(&content_keys);
}
EKey to ESpec Lookup
#![allow(unused)]
fn main() {
use cascette_crypto::EncodingKey;
if let Some(espec) = encoding.find_espec(&encoding_key) {
println!("Compression spec: {}", espec);
}
}
Building
#![allow(unused)]
fn main() {
use cascette_formats::encoding::{EncodingBuilder, CKeyEntryData, EKeyEntryData};
let mut builder = EncodingBuilder::new(); // 4KB pages
builder.add_ckey_entry(CKeyEntryData {
content_key,
file_size: 524_288,
encoding_keys: vec![encoding_key],
});
builder.add_ekey_entry(EKeyEntryData {
encoding_key,
espec: "z".to_string(),
file_size: 187_234,
});
let encoding_file = builder.build()?;
}
Page Structure
All pages are loaded eagerly. Each page preserves its original binary data for byte-exact round-trip reconstruction:
#![allow(unused)]
fn main() {
// Page<T> holds parsed entries and raw bytes
pub struct Page<T> {
pub entries: Vec<T>,
pub original_data: Vec<u8>,
}
// IndexEntry holds first key + MD5 checksum for integrity
pub struct IndexEntry {
pub first_key: [u8; 16],
pub checksum: [u8; 16],
}
}
All multi-byte header and page fields are big-endian.
ESpec Integration
The ESpec strings define how files are encoded:
Common Patterns
- Uncompressed: Empty string or
n - ZLib:
z - Partial compression:
b:{0,1000},z,b:{1000,500},n- Bytes 0-1000: ZLib compressed
- Bytes 1000-1500: Uncompressed
Parsing ESpec
#![allow(unused)]
fn main() {
enum ESpecOp {
None,
ZLib,
ByteRange { start: u32, size: u32 },
}
fn parse_espec(spec: &str) -> Vec<ESpecOp> {
if spec.is_empty() || spec == "n" {
return vec![ESpecOp::None];
}
spec.split(',')
.map(|part| match part {
"z" => ESpecOp::ZLib,
"n" => ESpecOp::None,
s if s.starts_with("b:") => {
// Parse "b:{start,size}"
let nums = parse_range(s);
ESpecOp::ByteRange {
start: nums.0,
size: nums.1
}
}
_ => ESpecOp::None,
})
.collect()
}
}
Multi-Version Support
Files can have multiple encoding keys (different compression/encryption):
#![allow(unused)]
fn main() {
struct CKeyEntry {
ekey_count: u8, // Usually 1, can be 2+
file_size: u64, // Same for all versions
ckey: [u8; 16], // Content key
ekeys: Vec<[u8; 16]>, // Multiple encoding keys
}
}
Use cases include different regional encryption and progressive quality levels.
Performance Considerations
Memory-Mapped Access
For large encoding files (100MB+):
#![allow(unused)]
fn main() {
use memmap2::MmapOptions;
struct EncodingFile {
mmap: Mmap,
header: EncodingHeader,
// ...
}
impl EncodingFile {
fn open(path: &Path) -> Result<Self> {
let file = File::open(path)?;
let mmap = unsafe { MmapOptions::new().map(&file)? };
// Parse header from mmap
let header = EncodingHeader::read(&mmap[..22])?;
Ok(Self { mmap, header })
}
}
}
Page Caching
Cache frequently accessed pages:
#![allow(unused)]
fn main() {
struct PageCache {
entries: LruCache<u32, Arc<CKeyPage>>,
}
}
Validation
Checksums
Each page has an MD5 checksum in the index:
#![allow(unused)]
fn main() {
fn validate_page(index: &PageIndex, data: &[u8]) -> bool {
let computed = md5::compute(data);
computed.0 == index.page_hash
}
}
Size Constraints
-
Page sizes must be > 0 (no power-of-2 requirement enforced)
-
Key sizes in range [1, 16] bytes
-
Page counts must be > 0
-
ESpec size must be > 0
-
File sizes use 40-bit integers (up to 1TB)
File’s Own ESpec
After all the data structures, the encoding file contains its own ESpec string describing how it’s compressed. This self-referential metadata is an intentional, documented feature of the NGDP format.
Official Documentation
The wowdev.wiki TACT specification explicitly lists this as the 5th component:
- Header
- Encoding specification data (ESpec)
- Content key → encoding key table
- Encoding key → encoding spec table
- “Encoding specification data for the encoding file itself”
Reference Implementation
TACT.Net explicitly handles this in EncodingFile.cs:
-
Line 151:
// remainder is an ESpec block for the file itself -
Implements
GetFileESpec()method to generate this when writing
Real-World Examples
wow_classic 5.5.0.62655 (60 bytes):
b:{22=n,76025=z,223424=n,28598272=n,146656=n,18771968=n,*=z}
wow_classic_era 1.15.7.61582 (55 bytes):
b:{22=n,2069=z,65536=n,8388608=n,43008=n,5505024=n,*=z}
Meaning:
-
22=n: Header (22 bytes) uncompressed -
76025=z: ESpec table compressed with ZLib -
223424=n: CKey index uncompressed -
28598272=n: CKey pages uncompressed -
146656=n: EKey index uncompressed -
18771968=n: EKey pages uncompressed -
*=z: Remainder (the file’s own ESpec) compressed
This self-referential design allows files to describe their own compression structure using the same ESpec format as all other files.
Common Issues
- Page Boundary Errors: Entries can span pages
- Endianness: All multi-byte values are big-endian
- ESpec Index: Zero-based into string table
- CKey Padding: Entries with
ekey_count = 0indicate end of page data - EKey Padding: Entries with
espec_index = 0xFFFFFFFFor all-zero keys indicate padding (see Padding Detection above) - File Size: Remember to account for the file’s own ESpec at the end
Real-World Example
Using wow_classic_era 1.15.7.61582:
Encoding file: bbf06e7476382cfaa396cff0049d356b
Header:
Magic: 0x454E ('EN')
Version: 1
CKey/EKey size: 16 bytes each
CKey pages: 4KB × 127 pages
EKey pages: 4KB × 127 pages
ESpec table: 1,234 bytes
Example CKey entry:
Content Key: 3ce96e7a9e3b6f5c9d99c8b4e0a4f3d2
EKey count: 1
File size: 524,288 bytes (512KB)
Encoding Key: 7f8a9b3c4d5e6f7081929a3b4c5d6e7f
Corresponding EKey entry:
Encoding Key: 7f8a9b3c4d5e6f7081929a3b4c5d6e7f
ESpec index: 1 (points to "z" - ZLib)
Compressed size: 187,234 bytes
This shows a typical game asset compressed from 512KB to 183KB using ZLib.
Implementation Flow
#![allow(unused)]
fn main() {
use cascette_formats::encoding::EncodingFile;
use cascette_crypto::ContentKey;
// 1. Parse encoding file from BLTE-encoded CDN data
let encoding = EncodingFile::parse_blte(&cdn_data)?;
// 2. Look up content by content key
let ekey = encoding.find_encoding(&content_key)
.ok_or("content key not found")?;
// 3. Optionally get the compression spec
let espec = encoding.find_espec(&ekey);
// 4. Fetch actual file from CDN using encoding key, then decompress
}
Version History
The Encoding file format currently has only one version:
Version 1 (Current)
- Header Size: 22 bytes
- Magic: “EN” (0x454E)
- Features:
- Content key to encoding key mapping
- Dual page index system (CKey and EKey pages)
- ESpec string table for compression metadata
- 40-bit file sizes (up to 1TB per file)
- Multiple encoding keys per content key support
- Page-based binary search
- MD5 page checksums for integrity
Version Detection
All known encoding files use version 1. The version field is at offset 2 in the header. If future versions are introduced, parsers should check this field after validating the “EN” magic bytes.
References
-
See ESpec Documentation for encoding specifications
-
See BLTE Format for container structure
-
See CDN Architecture for retrieval patterns
-
See Format Transitions for format evolution tracking