Skip to content

Commit a2d5d05

Browse files
Merge pull request #29 from wowemulation-dev/fix/agent-verified-parser-bugs
fix: correct format parser bugs verified against Agent.exe
2 parents f331118 + 3d750ce commit a2d5d05

24 files changed

Lines changed: 814 additions & 203 deletions

File tree

crates/cascette-formats/src/archive/index.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ impl IndexFooter {
302302

303303
/// Validate footer format parameters for CASC
304304
pub fn validate_format(&self) -> ArchiveResult<()> {
305-
if self.version != 1 {
305+
if self.version > 1 {
306306
return Err(ArchiveError::UnsupportedVersion(self.version));
307307
}
308308

@@ -1323,11 +1323,15 @@ mod tests {
13231323
// Valid format
13241324
assert!(footer.validate_format().is_ok());
13251325

1326-
// Invalid version
1327-
footer.version = 0x08;
1326+
// Version 0 is accepted
1327+
footer.version = 0;
1328+
assert!(footer.validate_format().is_ok());
1329+
1330+
// Version > 1 is rejected
1331+
footer.version = 2;
13281332
assert!(matches!(
13291333
footer.validate_format(),
1330-
Err(ArchiveError::UnsupportedVersion(0x08))
1334+
Err(ArchiveError::UnsupportedVersion(2))
13311335
));
13321336

13331337
// Reset and test invalid ekey length (outside valid range 9-16)
@@ -2397,7 +2401,7 @@ mod tests {
23972401
/// Test that invalid version numbers are rejected
23982402
fn invalid_version_rejected(
23992403
mut footer in index_footer(),
2400-
invalid_version in prop::num::u8::ANY.prop_filter("Not version 1", |&v| v != 1)
2404+
invalid_version in prop::num::u8::ANY.prop_filter("Not version 0 or 1", |&v| v > 1)
24012405
) {
24022406
footer.version = invalid_version;
24032407
prop_assert!(footer.validate_format().is_err());

crates/cascette-formats/src/blte/header.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,9 @@ impl BlteHeader {
164164
chunk_infos,
165165
};
166166

167-
// Calculate header size: 4 (flags + chunk_count) + (chunk_count * chunk_info_size)
168-
let header_size = 4 + (chunks.len() * extended.flags.chunk_info_size());
167+
// Calculate header size: 8 (magic + header_size field) + 4 (flags + chunk_count) + entries
168+
// The on-disk header_size includes the 8-byte BLTE preamble
169+
let header_size = 12 + (chunks.len() * extended.flags.chunk_info_size());
169170

170171
Ok(Self {
171172
magic: BLTE_MAGIC,
@@ -193,8 +194,8 @@ impl BlteHeader {
193194
if self.is_single_chunk() {
194195
8 // Just magic + header_size
195196
} else {
196-
// Standard format: data starts after header
197-
8 + self.header_size as usize
197+
// header_size already includes the 8-byte preamble
198+
self.header_size as usize
198199
}
199200
}
200201

@@ -203,7 +204,8 @@ impl BlteHeader {
203204
if self.is_single_chunk() {
204205
8 // magic (4) + header_size (4)
205206
} else {
206-
8 + self.header_size as usize // magic (4) + header_size (4) + extended header
207+
// header_size already includes the 8-byte preamble
208+
self.header_size as usize
207209
}
208210
}
209211
}
@@ -435,8 +437,8 @@ mod tests {
435437
}
436438

437439
// Calculate expected header size for extended format
438-
// 4 (flags + chunk_count) + 2 chunks * 40 bytes per chunk
439-
let expected_header_size: u32 = 4 + (2 * 40);
440+
// 8 (magic + header_size) + 4 (flags + chunk_count) + 2 chunks * 40 bytes per chunk
441+
let expected_header_size: u32 = 12 + (2 * 40);
440442
assert_eq!(header.header_size, expected_header_size);
441443
}
442444

crates/cascette-formats/src/blte/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,13 +378,16 @@ mod tests {
378378
let expected_size = if header.is_single_chunk() {
379379
8 // magic + header_size
380380
} else {
381-
4 + (chunk_count * flags.chunk_info_size()) // flags + count + chunk_infos
381+
// header_size includes 8-byte preamble + 4 (flags + count) + chunk_infos
382+
12 + (chunk_count * flags.chunk_info_size())
382383
};
383384

384385
if !header.is_single_chunk() {
385386
prop_assert_eq!(header.header_size as usize, expected_size);
386387
}
387-
prop_assert_eq!(header.data_offset(), 8 + header.header_size as usize);
388+
// For single-chunk: data starts at offset 8
389+
// For multi-chunk: header_size already includes the preamble
390+
prop_assert_eq!(header.data_offset(), if header.is_single_chunk() { 8 } else { header.header_size as usize });
388391
}
389392

390393
/// Test that checksums are deterministic

crates/cascette-formats/src/download/header.rs

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ pub struct DownloadHeaderV3 {
6868
pub flag_size: u8,
6969
/// Base priority adjustment (signed byte)
7070
pub base_priority: i8,
71-
/// Reserved bytes (must be zero)
72-
#[br(assert(reserved == [0, 0, 0], "Reserved field must be zero, got: {:?}", reserved))]
71+
/// Reserved bytes (not validated)
7372
pub reserved: [u8; 3],
7473
}
7574

@@ -155,9 +154,9 @@ impl DownloadHeader {
155154
/// Calculate total header size in bytes
156155
pub fn header_size(&self) -> usize {
157156
match self {
158-
Self::V1(_) => 10, // magic(2) + version(1) + ekey_length(1) + has_checksum(1) + entry_count(4) + tag_count(2)
159-
Self::V2(_) => 11, // V1 + flag_size(1)
160-
Self::V3(_) => 15, // V2 + base_priority(1) + reserved(3)
157+
Self::V1(_) => 11, // magic(2) + version(1) + ekey_length(1) + has_checksum(1) + entry_count(4) + tag_count(2)
158+
Self::V2(_) => 12, // V1 + flag_size(1)
159+
Self::V3(_) => 16, // V2 + base_priority(1) + reserved(3)
161160
}
162161
}
163162

@@ -185,19 +184,6 @@ impl DownloadHeader {
185184
return Err(DownloadError::InvalidEncodingKeyLength(ekey_length));
186185
}
187186

188-
// Version-specific validation
189-
match self {
190-
Self::V1(_) | Self::V2(_) => {
191-
// V1/V2 have no additional fields to validate
192-
}
193-
Self::V3(h) => {
194-
// V3 validation - reserved field should be zero
195-
if h.reserved != [0, 0, 0] {
196-
return Err(DownloadError::ReservedFieldNotZero(h.reserved));
197-
}
198-
}
199-
}
200-
201187
Ok(())
202188
}
203189

@@ -282,14 +268,6 @@ impl BinRead for DownloadHeader {
282268
let mut reserved = [0u8; 3];
283269
reader.read_exact(&mut reserved)?;
284270

285-
// Validate reserved field
286-
if reserved != [0, 0, 0] {
287-
return Err(binrw::Error::Custom {
288-
pos: reader.stream_position().unwrap_or(0),
289-
err: Box::new(DownloadError::ReservedFieldNotZero(reserved)),
290-
});
291-
}
292-
293271
Ok(Self::V3(DownloadHeaderV3 {
294272
magic: base.magic,
295273
version: base.version,
@@ -343,7 +321,7 @@ mod tests {
343321
assert!(header.has_checksum());
344322
assert_eq!(header.flag_size(), 0);
345323
assert_eq!(header.base_priority(), 0);
346-
assert_eq!(header.header_size(), 10);
324+
assert_eq!(header.header_size(), 11);
347325
}
348326

349327
#[test]
@@ -356,7 +334,7 @@ mod tests {
356334
assert!(!header.has_checksum());
357335
assert_eq!(header.flag_size(), 2);
358336
assert_eq!(header.base_priority(), 0);
359-
assert_eq!(header.header_size(), 11);
337+
assert_eq!(header.header_size(), 12);
360338
}
361339

362340
#[test]
@@ -369,7 +347,7 @@ mod tests {
369347
assert!(header.has_checksum());
370348
assert_eq!(header.flag_size(), 1);
371349
assert_eq!(header.base_priority(), -5);
372-
assert_eq!(header.header_size(), 15);
350+
assert_eq!(header.header_size(), 16);
373351
}
374352

375353
#[test]
@@ -540,7 +518,7 @@ mod tests {
540518
}
541519

542520
#[test]
543-
fn test_invalid_reserved_field() {
521+
fn test_nonzero_reserved_field_accepted() {
544522
let data = [
545523
b'D', b'L', // Magic
546524
3, // Version
@@ -550,11 +528,15 @@ mod tests {
550528
0, 5, // Tag count
551529
0, // Flag size
552530
0, // Base priority
553-
1, 2, 3, // Invalid reserved field (should be 0, 0, 0)
531+
1, 2, 3, // Non-zero reserved field (accepted per Agent behavior)
554532
];
555533

556-
let result = DownloadHeader::read_options(&mut Cursor::new(&data), binrw::Endian::Big, ());
534+
let header = DownloadHeader::read_options(&mut Cursor::new(&data), binrw::Endian::Big, ())
535+
.expect("Non-zero reserved bytes should be accepted");
557536

558-
assert!(result.is_err());
537+
assert_eq!(header.version(), 3);
538+
assert_eq!(header.entry_count(), 10);
539+
// validate() also should not reject non-zero reserved
540+
assert!(header.validate().is_ok());
559541
}
560542
}

crates/cascette-formats/src/download/manifest.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,8 @@ mod tests {
463463
assert_eq!(original, parsed);
464464

465465
// Validate layout by checking specific positions in binary data
466-
// After header (12 bytes for V2: 11 base + 1 flag_size), entries should come first
467-
let header_size = 12; // V2 header size (11 + 1 byte for flag_size)
466+
// After header (12 bytes for V2), entries should come first
467+
let header_size = 12; // V2 header size
468468
let entry_size = 16 + 5 + 1 + 4 + 1; // ekey + size + priority + checksum + flags
469469
let entries_section_size = 2 * entry_size; // 2 entries
470470
let tags_start_offset = header_size + entries_section_size;

crates/cascette-formats/src/espec/parser.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ impl<'a> Parser<'a> {
151151
let level = if self.peek().is_some_and(|c| c.is_ascii_digit()) {
152152
let num = self.parse_number()?;
153153
let level = u8::try_from(num).map_err(|_| ESpecError::InvalidLevel(255))?;
154-
if level > 9 {
154+
if level == 0 || level > 9 {
155155
return Err(ESpecError::InvalidLevel(level));
156156
}
157157
Some(level)
@@ -513,7 +513,13 @@ mod tests {
513513
Err(ESpecError::UnknownType('x'))
514514
));
515515

516-
// Invalid compression level
516+
// Invalid compression level (0 is rejected)
517+
assert!(matches!(
518+
ESpec::parse("z:0"),
519+
Err(ESpecError::InvalidLevel(0))
520+
));
521+
522+
// Invalid compression level (10 is rejected)
517523
assert!(matches!(
518524
ESpec::parse("z:10"),
519525
Err(ESpecError::InvalidLevel(10))

crates/cascette-formats/src/espec/types.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub enum ESpecError {
1313
UnknownType(char),
1414

1515
/// Invalid compression level
16-
#[error("Invalid compression level {0}, must be 0-9")]
16+
#[error("Invalid compression level {0}, must be 1-9")]
1717
InvalidLevel(u8),
1818

1919
/// Invalid window bits
@@ -57,7 +57,7 @@ pub enum ESpec {
5757

5858
/// `ZLib` compression ('z')
5959
ZLib {
60-
/// Compression level (0-9)
60+
/// Compression level (1-9)
6161
level: Option<u8>,
6262
/// Window bits or special mode
6363
bits: Option<ZLibBits>,

crates/cascette-formats/src/patch_archive/header.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub struct PatchArchiveHeader {
1717
#[br(assert(magic == *b"PA", "Invalid PA magic"))]
1818
pub magic: [u8; 2],
1919

20-
/// Format version, typically 2
20+
/// Format version (1 or 2)
2121
pub version: u8,
2222

2323
/// Size of file content keys in bytes (16 for MD5)
@@ -65,7 +65,7 @@ impl PatchArchiveHeader {
6565
return Err(PatchArchiveError::InvalidMagic(self.magic));
6666
}
6767

68-
if self.version != 2 {
68+
if self.version == 0 || self.version > 2 {
6969
return Err(PatchArchiveError::UnsupportedVersion(self.version));
7070
}
7171

@@ -77,7 +77,7 @@ impl PatchArchiveHeader {
7777
});
7878
}
7979

80-
if self.block_size_bits < 10 || self.block_size_bits > 20 {
80+
if self.block_size_bits < 12 || self.block_size_bits > 24 {
8181
return Err(PatchArchiveError::InvalidBlockSize(self.block_size_bits));
8282
}
8383

@@ -132,7 +132,15 @@ mod tests {
132132
fn test_header_validation() {
133133
let mut header = PatchArchiveHeader::new(1);
134134

135-
// Test invalid version
135+
// Test version 0 is invalid
136+
header.version = 0;
137+
assert!(header.validate().is_err());
138+
139+
// Test version 1 is valid
140+
header.version = 1;
141+
assert!(header.validate().is_ok());
142+
143+
// Test version 3 is invalid
136144
header.version = 3;
137145
assert!(header.validate().is_err());
138146

@@ -143,10 +151,16 @@ mod tests {
143151

144152
// Test invalid block size
145153
header.file_key_size = 16;
146-
header.block_size_bits = 5; // Too small
154+
header.block_size_bits = 11; // Too small (min is 12)
147155
assert!(header.validate().is_err());
148156

149-
header.block_size_bits = 25; // Too large
157+
header.block_size_bits = 25; // Too large (max is 24)
150158
assert!(header.validate().is_err());
159+
160+
// Test boundary values are accepted
161+
header.block_size_bits = 12;
162+
assert!(header.validate().is_ok());
163+
header.block_size_bits = 24;
164+
assert!(header.validate().is_ok());
151165
}
152166
}

crates/cascette-formats/src/tvfs/header.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ pub const TVFS_FLAG_INCLUDE_CKEY: u32 = 0x01;
1010
pub const TVFS_FLAG_WRITE_SUPPORT: u32 = 0x02;
1111
/// Patch support enabled
1212
pub const TVFS_FLAG_PATCH_SUPPORT: u32 = 0x04;
13-
/// Encoding specification table present (combined write and patch support)
14-
pub const TVFS_FLAG_ENCODING_SPEC: u32 = TVFS_FLAG_WRITE_SUPPORT | TVFS_FLAG_PATCH_SUPPORT;
13+
/// Encoding specification table present (write support flag only)
14+
pub const TVFS_FLAG_ENCODING_SPEC: u32 = TVFS_FLAG_WRITE_SUPPORT;
1515

16-
/// TVFS file header (46 bytes for modern builds)
16+
/// TVFS file header (38 bytes base, 46 bytes with encoding spec table)
1717
#[derive(Debug, Clone, BinRead, BinWrite)]
1818
#[br(big)] // Big-endian
1919
#[bw(big)]
@@ -25,7 +25,7 @@ pub struct TvfsHeader {
2525
/// Format version (always 1)
2626
pub format_version: u8,
2727

28-
/// Header size (always 46)
28+
/// Header size (38 without EST, 46 with EST)
2929
pub header_size: u8,
3030

3131
/// EKey size (always 9)
@@ -76,9 +76,9 @@ impl TvfsHeader {
7676
magic: *b"TVFS",
7777
format_version: 1,
7878
header_size: if flags & TVFS_FLAG_ENCODING_SPEC != 0 {
79-
54
80-
} else {
8179
46
80+
} else {
81+
38
8282
},
8383
ekey_size: 9,
8484
pkey_size: 9,
@@ -110,9 +110,9 @@ impl TvfsHeader {
110110
}
111111

112112
let expected_header_size = if self.flags & TVFS_FLAG_ENCODING_SPEC != 0 {
113-
54
114-
} else {
115113
46
114+
} else {
115+
38
116116
};
117117
if self.header_size != expected_header_size {
118118
return Err(TvfsError::InvalidHeaderSize(self.header_size));

crates/cascette-protocol/src/client/tact.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ impl TactClient {
2121
/// Create a new TACT client
2222
#[cfg(not(target_arch = "wasm32"))]
2323
pub fn new(base_url: String, _use_https: bool) -> Result<Self> {
24+
crate::transport::ensure_crypto_provider();
2425
let client = Client::builder()
2526
.pool_idle_timeout(Duration::from_secs(90))
2627
.pool_max_idle_per_host(10)

0 commit comments

Comments
 (0)