livesplit_core/run/parser/
llanfair.rs1use crate::{
4 util::byte_parsing::{
5 big_endian::{strip_u16, strip_u32, strip_u64},
6 strip_slice, strip_u8,
7 },
8 RealTime, Run, Segment, Time, TimeSpan,
9};
10use core::{result::Result as StdResult, str};
11#[cfg(feature = "std")]
12use image::{codecs::png, ColorType, ImageBuffer, ImageEncoder, Rgba};
13use snafu::{OptionExt, ResultExt};
14
15#[derive(Debug, snafu::Snafu)]
18#[snafu(context(suffix(false)))]
19pub enum Error {
20 ReadHeader,
22 InvalidHeader,
24 DetermineLength,
26 SkipToGoal,
28 ReadGoal {
30 source: StringError,
32 },
33 SkipToTitle,
35 ReadTitle {
37 source: StringError,
39 },
40 ReadSegmentCount,
42 ReadSegment,
44 ReadBestSegment,
46 ReadIcon,
48 #[snafu(display("The dimensions {width}x{height} of a segment's icon are not valid."))]
50 InvalidIconDimensions {
51 width: u32,
53 height: u32,
55 },
56 ReadImageData,
58 ReadSegmentName {
60 source: StringError,
62 },
63 ReadSegmentTime,
65}
66
67#[derive(Debug, snafu::Snafu)]
69#[snafu(context(suffix(false)))]
70pub enum StringError {
71 ReadLength,
73 LengthOutOfBounds,
75 Validate,
77}
78
79pub type Result<T> = StdResult<T, Error>;
81
82fn to_time(milliseconds: u64) -> Time {
83 if milliseconds == 0 {
84 Time::default()
85 } else {
86 RealTime(Some(TimeSpan::from_milliseconds(milliseconds as f64))).into()
87 }
88}
89
90fn read_string<'a>(cursor: &mut &'a [u8]) -> StdResult<&'a str, StringError> {
91 let str_length = strip_u16(cursor).context(ReadLength)? as usize;
92 let str_data = strip_slice(cursor, str_length).context(LengthOutOfBounds)?;
93 simdutf8::basic::from_utf8(str_data).ok().context(Validate)
94}
95
96pub fn parse(source: &[u8]) -> Result<Run> {
98 #[cfg(feature = "std")]
99 let mut buf = Vec::new();
100
101 const HEADER: &[u8; 30] = b"\xAC\xED\0\x05\x73\x72\0\x16org.fenix.llanfair.Run";
110
111 if !source.starts_with(HEADER) {
112 return Err(Error::InvalidHeader);
113 }
114
115 let mut run = Run::new();
116
117 let mut cursor = source.get(0xc5..).context(SkipToGoal)?;
119 run.set_category_name(read_string(&mut cursor).context(ReadGoal)?);
120
121 cursor = cursor.get(1..).context(SkipToTitle)?;
123 run.set_game_name(read_string(&mut cursor).context(ReadTitle)?);
124
125 cursor = cursor.get(0x6..).context(ReadSegmentCount)?;
126 let segment_count = strip_u32(&mut cursor).context(ReadSegmentCount)?;
127
128 let (mut time_encountered, mut icon_encountered) = (false, false);
133
134 let mut aggregate_time_ms = 0;
135
136 cursor = cursor.get(0x8F..).context(ReadSegment)?;
138
139 run.segments_mut().reserve(segment_count as usize);
140
141 for _ in 0..segment_count {
142 #[cfg(feature = "std")]
143 let mut icon = None;
144 let mut best_segment_ms = 0;
145
146 let id = strip_u8(&mut cursor).context(ReadSegment)?;
147 if id != 0x70 {
148 if !time_encountered {
149 time_encountered = true;
150
151 cursor = cursor.get(0x36..).context(ReadSegment)?;
153 } else {
154 cursor = cursor.get(0x5..).context(ReadSegment)?;
155 }
156
157 best_segment_ms = strip_u64(&mut cursor).context(ReadBestSegment)?;
158 }
159
160 let id = strip_u8(&mut cursor).context(ReadIcon)?;
161 if id != 0x70 {
162 let seek_offset_base = if !icon_encountered {
163 icon_encountered = true;
164 cursor = cursor.get(0xBC..).context(ReadIcon)?;
165 0x25
166 } else {
167 cursor = cursor.get(0x5..).context(ReadIcon)?;
168 0x18
169 };
170 let height = strip_u32(&mut cursor).context(ReadIcon)?;
171 let width = strip_u32(&mut cursor).context(ReadIcon)?;
172
173 cursor = cursor.get(seek_offset_base..).context(ReadIcon)?;
174
175 let len = (width as usize)
176 .checked_mul(height as usize)
177 .and_then(|b| b.checked_mul(4))
178 .context(InvalidIconDimensions { width, height })?;
179
180 if cursor.len() < len {
181 return Err(Error::InvalidIconDimensions { width, height });
182 }
183
184 let (_image_data, rem) = cursor.split_at(len);
185 cursor = rem;
186
187 #[cfg(feature = "std")]
188 if let Some(image) = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, _image_data) {
189 buf.clear();
190 if png::PngEncoder::new(&mut buf)
191 .write_image(image.as_ref(), width, height, ColorType::Rgba8)
192 .is_ok()
193 {
194 icon = Some(crate::settings::Image::new(&buf));
195 }
196 }
197 }
198
199 cursor = cursor.get(1..).context(ReadSegment)?;
201 let mut segment = Segment::new(read_string(&mut cursor).context(ReadSegmentName)?);
202
203 #[cfg(feature = "std")]
204 if let Some(icon) = icon {
205 segment.set_icon(icon);
206 }
207
208 let id = strip_u8(&mut cursor).context(ReadSegmentTime)?;
210 let segment_time_ms = match id {
211 0x71 => {
212 cursor = cursor.get(0x4..).context(ReadSegmentTime)?;
213 best_segment_ms
214 }
215 0x70 => 0,
216 _ => {
217 cursor = cursor.get(0x5..).context(ReadSegmentTime)?;
221 strip_u64(&mut cursor).context(ReadSegmentTime)?
222 }
223 };
224
225 if segment_time_ms != 0 {
226 aggregate_time_ms += segment_time_ms;
227 let split_time = to_time(aggregate_time_ms);
228 segment.set_personal_best_split_time(split_time);
229 }
230
231 segment.set_best_segment_time(to_time(best_segment_ms));
232
233 run.push_segment(segment);
234
235 cursor = cursor.get(0x6..).context(ReadSegment)?;
237 }
238
239 Ok(run)
240}