1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/*!
Rust library and tool to parse and inspect ROFL replay files generated from League of Legends games.

Backward-compatibility for replay files is NOT to be expected as of now.

# Usage as a command-line tool

After building with `cargo install --bin lolrofl --features "clap json payload"`, a new `lolrofl` executable become available.

Said executable allows the inspection of ROFL files to extract game information, metadata, or development intel with the following commands:

* `get`: Get high-level information on the file
  * `get info`: Print simple/high-level info on the file and the game
  * `get metadata`: Print the game's metadata
  * `get payload`: Print technical information on the file
* `analyze`: Get low-level information on the file - usually for debug and development purpose
* `export`: Export chunk or keyframe data to a file or directory

# Usage as a library

Use `lolrofl` to parse a loaded file's content :

```rust
// Parse the file's content with Rofl
# let content = lolrofl::test::sample_base_file_0();
// let content = std::fs::read(source_file).unwrap();
let data = lolrofl::Rofl::from_slice(&content[..]).unwrap();

// Print the file's length as specified within the file (this may not match the actual file size if it is incomplete or corrupted)
println!("Expected file length: {:?} bytes", data.head().file_len());
// Print the file's metadata (a JSON string)
println!("{}", data.metadata().unwrap());
// Print information on the game without depending on the metadata
let payload = data.payload().unwrap();
println!("The game {} lasted {} seconds", payload.id(), payload.duration()/1000);
# assert_eq!(payload.duration(), 91722);
```
*/

mod error;
pub use error::*;
pub mod iter;
pub mod model;
// FIXME: the test feature is only required because doctest context is not passed by cargo at compile-time
#[cfg(any(doctest, test, feature = "test"))]
pub mod test;
use model::*;

/// Base ROFL file parser
/// 
/// # Usage
/// 
/// ```rust
/// // Parse a file's content
/// # let content = lolrofl::test::sample_base_file_0();
/// // let content = std::fs::read("game.rofl").unwrap();
/// let game = lolrofl::Rofl::from_slice(&content[..]).unwrap();
///
/// let header = game.head(); // File header
/// let data = game.metadata(); // Game metadata JSON string
/// # assert_eq!(data.is_ok(), true);
/// let payload = game.payload(); // Game payload
/// # assert_eq!(payload.is_ok(), true);
/// ```
pub struct Rofl<'a> {
    /// ROFL file's Start Header
    head: BinHeader,
    /// ROFL File's data
    data: &'a[u8],
}

impl Rofl<'_> {
    /// Starting bytes of a ROFL file
    /// 
    /// This is public for ease of file recognition but should generally NOT be relied upon
    pub const MAGIC: [u8; 4] = [82,73,79,84]; // TODO: check if 6 bytes instead of 0
    /// Get the ROFL header
    /// 
    /// # Examples
    /// 
    /// ```rust
    /// # let content = lolrofl::test::sample_base_file_0();
    /// // let content = std::fs::read("game.rofl").unwrap();
    /// let game = lolrofl::Rofl::from_slice(&content[..]).unwrap();
    /// println!("Expected file length: {} bytes", game.head().file_len());
    /// # assert_eq!(game.head().file_len(), 0x01cd);
    /// ```
    pub fn head(&self) -> &BinHeader { &self.head }
    /// Get the loaded JSON Metadata string
    /// 
    /// # Warning
    /// 
    /// The returned string is not guaranteed to be valid if the file is malformed
    /// 
    /// # Examples
    /// 
    /// ```rust
    /// # let content = lolrofl::test::sample_base_file_0();
    /// // let content = std::fs::read("game.rofl").unwrap();
    /// let game = lolrofl::Rofl::from_slice(&content[..]).unwrap();
    /// let meta = json::parse(game.metadata().unwrap()).unwrap();
    /// println!("Duration: {} ms", meta["gameLength"]);
    /// println!("Patch: {}", meta["gameVersion"]);
    /// # assert_eq!(meta["gameVersion"], "12.10.444.2068");
    /// ```
    pub fn metadata(&self) -> Result<&str, Errors> {
        if self.data.len() < self.head.metadata_offset() + self.head.metadata_len() {
            return Err(error::Errors::BufferTooSmall);
        }
        std::str::from_utf8(
                &self.data[self.head.metadata_offset()..self.head.metadata_offset() + self.head.metadata_len()]
        )
        .or_else(|_| Err(error::Errors::InvalidBuffer))
    }
    /// Get the loaded payload header
    /// 
    /// # Examples
    /// 
    /// ```rust
    /// # let content = lolrofl::test::sample_base_file_0();
    /// // let content = std::fs::read("game.rofl").unwrap();
    /// let game = lolrofl::Rofl::from_slice(&content[..]).unwrap();
    ///
    /// let payload = game.payload().unwrap();
    /// println!("Game ID: {}", payload.id());
    /// println!("Duration: {} ms", payload.duration());
    /// # assert_eq!(payload.duration(), 0x01664a);
    /// # assert_eq!(payload.chunk_count(), 6);
    /// ```
    pub fn payload(&self) -> Result<PayloadHeader, Errors> {
        if self.data.len() < self.head.payload_header_offset() + self.head.payload_header_len() {
            Err(Errors::BufferTooSmall)
        } else {
            let payload = PayloadHeader::from_raw_section(
                &self.data[self.head.payload_header_offset()..self.head.payload_header_offset() + self.head.payload_header_len()]
            );
            Ok(payload)
        }
    }
    /// Get an iterator over the payload's segments
    /// 
    /// `with_data` is implicitly `false` if the lib was compiled without the `payload` feature
    /// 
    /// # Examples
    /// 
    /// ```ignore
    /// # let content = lolrofl::test::sample_base_file_0();
    /// // let content = std::fs::read("game.rofl").unwrap();
    /// let game = lolrofl::Rofl::from_slice(&content[..]).unwrap();
    ///
    /// for segment in game.segment_iter(false).unwrap() {
    ///     println("{} {}", if segment.is_chunk() { "Chunk" } else { "Keyframe" }, segment.id())
    /// }
    /// ```
    /// 
    /// ```rust
    /// // Truncated file
    /// # let content = lolrofl::test::sample_base_file_0();
    /// // let content = std::fs::read("game.rofl").unwrap();
    /// let game = lolrofl::Rofl::from_slice(&content[..]).unwrap();
    ///
    /// let mut data = game.segment_iter(false);
    /// assert_eq!(data.is_err(), true)
    /// ```
    pub fn segment_iter<'a>(&'a self, with_data: bool) -> Result<crate::iter::PayloadIterator<'a>, error::Errors> {
        // FIXME: the doctest should be runnable
        if self.data.len() < self.head.file_len() {
            Err(error::Errors::BufferTooSmall)
        } else {
            self.payload().and_then(|p|
                crate::iter::PayloadIterator::new(
                    &self.data[self.head.payload_offset()..self.head.file_len()],
                    &p,
                    with_data,
                )
            )
        }
    }
    /// Create a new Rofl instance from a ROFL file's slice
    /// 
    /// # Panics
    /// 
    /// If the buffer contains less than 288 bytes - in the future, this will be an error
    /// 
    /// # Errors
    /// 
    /// If the slice does not start with [`MAGIC`]
    /// 
    /// [`MAGIC`]: Rofl::MAGIC
    /// 
    /// # Examples
    /// 
    /// ```rust
    /// # let content = lolrofl::test::sample_base_file_0();
    /// // let content = std::fs::read("game.rofl").unwrap();
    /// let game = lolrofl::Rofl::from_slice(&content[..]).unwrap();
    /// ```
    pub fn from_slice<'a>(slice: &'a[u8]) -> Result<Rofl<'a>, Errors> {
        if slice.len() < Rofl::MAGIC.len() || Rofl::MAGIC != slice[..Rofl::MAGIC.len()] {
            return Err(Errors::InvalidBuffer);
        }
        // FIXME: return Result<> in BinHeader initializers and control slice size
        let header = BinHeader::from_raw_source(slice);

        Ok(Rofl {
            head: header,
            data: slice,
        })
    }
}