Skip to content

Commit b582eb6

Browse files
committed
Implement replay format detection. Add test data files in LFS.
1 parent c35d7cc commit b582eb6

File tree

13 files changed

+291
-1
lines changed

13 files changed

+291
-1
lines changed

.editorconfig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
indent_size = 2
7+
indent_style = space
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true
10+
11+
[*.bat]
12+
end_of_line = crlf
13+
14+
[*.rs]
15+
indent_size = 4

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
* text=auto eol=lf
2+
3+
*.rep filter=lfs diff=lfs merge=lfs -text

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
/target
2+
3+
CLAUDE.local.md
4+
.claude/settings.local.json

CLAUDE.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# CLAUDE.md
2+
3+
## Project Overview
4+
5+
This is a pure Rust library called `broodrep` for reading StarCraft 1 replay files, supporting all versions. The project uses a Cargo workspace structure with two main components:
6+
7+
- `broodrep/` - The core library crate
8+
- `broodrep-cli/` - A command-line interface that depends on the library
9+
10+
## Development Commands
11+
12+
### Building
13+
```bash
14+
cargo build # Build all workspace members
15+
cargo build -p broodrep # Build only the library
16+
cargo build -p broodrep-cli # Build only the CLI
17+
```
18+
19+
### Testing
20+
```bash
21+
cargo test # Run all tests in workspace
22+
cargo test -p broodrep # Test only the library
23+
```
24+
25+
### Running
26+
```bash
27+
cargo run -p broodrep-cli # Run the CLI application
28+
```
29+
30+
## Architecture
31+
32+
- The project is currently in early development.
33+
- Uses Rust 2024 edition
34+
- Is warning-free (and clippy lint free)
35+
36+
The workspace is structured as a typical Rust project where the CLI tool depends on the core library for replay parsing functionality.

Cargo.lock

Lines changed: 65 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# broodrep
2+
3+
A pure Rust library for reading StarCraft 1 replay files. Supports all versions.
4+
5+
## Development
6+
7+
Test data is stored in Git LFS. If you haven't used Git LFS before run:
8+
9+
```
10+
git lfs install
11+
```
12+
13+
## See also
14+
15+
- [broodmap](https://github.com/ShieldBattery/broodmap) - a pure Rust implementation of StarCraft 1 map parsing
16+
17+
## License
18+
19+
Licensed under either of
20+
21+
* Apache License, Version 2.0
22+
([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
23+
* MIT license
24+
([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
25+
26+
at your option.
27+
28+
## Contribution
29+
30+
Unless you explicitly state otherwise, any contribution intentionally submitted
31+
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
32+
dual licensed as above, without any additional terms or conditions.
33+

broodrep/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ edition = "2024"
55
license = "MIT OR Apache-2.0"
66

77
[dependencies]
8+
byteorder = "1.5"
9+
thiserror = "2.0"

broodrep/src/lib.rs

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,119 @@
1-
// TODO(tec27): build it
1+
use std::io::{Read, Seek, SeekFrom};
2+
3+
use byteorder::ReadBytesExt as _;
4+
use thiserror::Error;
5+
6+
#[derive(Error, Debug)]
7+
pub enum BroodrepError {
8+
#[error(transparent)]
9+
IoError(#[from] std::io::Error),
10+
#[error("malformed header: {0}")]
11+
MalformedHeader(&'static str),
12+
}
13+
14+
pub struct Replay<R: Read + Seek> {
15+
inner: R,
16+
format: ReplayFormat,
17+
}
18+
19+
impl<R: Read + Seek> Replay<R> {
20+
pub fn new(mut reader: R) -> Result<Self, BroodrepError> {
21+
let format = Self::detect_format(&mut reader)?;
22+
Ok(Replay {
23+
inner: reader,
24+
format,
25+
})
26+
}
27+
28+
pub fn into_inner(self) -> R {
29+
self.inner
30+
}
31+
32+
pub fn format(&self) -> ReplayFormat {
33+
self.format
34+
}
35+
36+
fn detect_format(reader: &mut R) -> Result<ReplayFormat, BroodrepError> {
37+
// 1.21+ has `seRS`, before that it's `reRS`
38+
reader.seek(SeekFrom::Start(12))?;
39+
let mut magic = [0; 4];
40+
reader.read(&mut magic)?;
41+
if magic == *b"seRS" {
42+
return Ok(ReplayFormat::Modern121);
43+
}
44+
if magic != *b"reRS" {
45+
return Err(BroodrepError::MalformedHeader("invalid magic bytes"));
46+
}
47+
48+
// Check compression type, newer compression type indicates 1.18+
49+
reader.seek(SeekFrom::Current(12))?; // offset 28
50+
let byte = reader.read_u8()?;
51+
if byte == 0x78 {
52+
Ok(ReplayFormat::Modern)
53+
} else {
54+
// TODO(tec27): Make sure it's within the valid range
55+
Ok(ReplayFormat::Legacy)
56+
}
57+
}
58+
}
59+
60+
/// The format version of a replay.
61+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
62+
pub enum ReplayFormat {
63+
/// The replay was created with a version before 1.18.
64+
Legacy,
65+
/// The replay was created with a version between 1.18 and 1.21.
66+
Modern,
67+
/// The replay was created with version 1.21 or later.
68+
Modern121,
69+
}
70+
71+
#[cfg(test)]
72+
mod tests {
73+
use std::io::Cursor;
74+
75+
use super::*;
76+
77+
const NOT_A_REPLAY: &[u8] = include_bytes!("../testdata/not_a_replay.rep");
78+
const LEGACY: &[u8] = include_bytes!("../testdata/things.rep");
79+
const LEGACY_EMPTY: &[u8] = include_bytes!("../testdata/empty.rep");
80+
const SCR_OLD: &[u8] = include_bytes!("../testdata/scr_old.rep");
81+
const SCR_121: &[u8] = include_bytes!("../testdata/scr_replay.rep");
82+
83+
#[test]
84+
fn test_replay_format_invalid() {
85+
let mut cursor = Cursor::new(NOT_A_REPLAY);
86+
assert!(matches!(
87+
Replay::new(&mut cursor),
88+
Err(BroodrepError::MalformedHeader(_))
89+
));
90+
}
91+
92+
#[test]
93+
fn test_replay_format_legacy() {
94+
let mut cursor = Cursor::new(LEGACY);
95+
let replay = Replay::new(&mut cursor).unwrap();
96+
assert_eq!(replay.format, ReplayFormat::Legacy);
97+
}
98+
99+
#[test]
100+
fn test_replay_format_legacy_empty() {
101+
let mut cursor = Cursor::new(LEGACY_EMPTY);
102+
let replay = Replay::new(&mut cursor).unwrap();
103+
assert_eq!(replay.format, ReplayFormat::Legacy);
104+
}
105+
106+
#[test]
107+
fn test_replay_format_scr_old() {
108+
let mut cursor = Cursor::new(SCR_OLD);
109+
let replay = Replay::new(&mut cursor).unwrap();
110+
assert_eq!(replay.format, ReplayFormat::Modern);
111+
}
112+
113+
#[test]
114+
fn test_replay_format_scr_121() {
115+
let mut cursor = Cursor::new(SCR_121);
116+
let replay = Replay::new(&mut cursor).unwrap();
117+
assert_eq!(replay.format, ReplayFormat::Modern121);
118+
}
119+
}

broodrep/testdata/empty.rep

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:b243f0e8e850b48e2e70de9ea409f7f1e306e538f242f19c670f77029060efb0
3+
size 291677

broodrep/testdata/not_a_replay.rep

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:92df6b4e7b78404a7258a8fc096343058728caee06a7551baf1a141be813264d
3+
size 38

0 commit comments

Comments
 (0)