livesplit_core/run/parser/
llanfair.rs

1//! Provides the parser for Llanfair splits files.
2
3use 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/// The Error type for splits files that couldn't be parsed by the Llanfair
16/// Parser.
17#[derive(Debug, snafu::Snafu)]
18#[snafu(context(suffix(false)))]
19pub enum Error {
20    /// Failed to read the header.
21    ReadHeader,
22    /// The Header doesn't match the header of a Llanfair file.
23    InvalidHeader,
24    /// Failed to determine the length of the file.
25    DetermineLength,
26    /// Failed to skip ahead to the goal.
27    SkipToGoal,
28    /// Failed to read the goal.
29    ReadGoal {
30        /// The underlying error.
31        source: StringError,
32    },
33    /// Failed to skip ahead to the title.
34    SkipToTitle,
35    /// Failed to read the title.
36    ReadTitle {
37        /// The underlying error.
38        source: StringError,
39    },
40    /// Failed to read the amount of segments.
41    ReadSegmentCount,
42    /// Failed to read the next segment.
43    ReadSegment,
44    /// Failed to read the best segment time of a segment.
45    ReadBestSegment,
46    /// Failed to read the icon of a segment.
47    ReadIcon,
48    /// The icon of a segment has invalid dimensions.
49    #[snafu(display("The dimensions {width}x{height} of a segment's icon are not valid."))]
50    InvalidIconDimensions {
51        /// The width of the icon.
52        width: u32,
53        /// The height of the icon.
54        height: u32,
55    },
56    /// Failed to read the data of a segment's icon.
57    ReadImageData,
58    /// Failed to read the name of a segment.
59    ReadSegmentName {
60        /// The underlying error.
61        source: StringError,
62    },
63    /// Failed to read the segment time of a segment.
64    ReadSegmentTime,
65}
66
67/// An error type that indicates that a string failed to be parsed.
68#[derive(Debug, snafu::Snafu)]
69#[snafu(context(suffix(false)))]
70pub enum StringError {
71    /// Failed to read the length of the string.
72    ReadLength,
73    /// The string was larger than the total remaining splits file.
74    LengthOutOfBounds,
75    /// The string is not encoded as valid UTF-8.
76    Validate,
77}
78
79/// The Result type for the Llanfair Parser.
80pub 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
96/// Attempts to parse a Llanfair splits file.
97pub fn parse(source: &[u8]) -> Result<Run> {
98    #[cfg(feature = "std")]
99    let mut buf = Vec::new();
100
101    // The protocol is documented here:
102    // https://docs.oracle.com/javase/7/docs/platform/serialization/spec/protocol.html
103
104    // AC ED  Magic
105    // 00 05  Version
106    // 73     New Object
107    // 72     New Class Declaration
108    // 00 16  Length of Class Name
109    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    // Skip to the goal string
118    let mut cursor = source.get(0xc5..).context(SkipToGoal)?;
119    run.set_category_name(read_string(&mut cursor).context(ReadGoal)?);
120
121    // Skip to the title string
122    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    // The object header changes if there is no instance of one of the object
129    // used by the Run class. The 2 objects that can be affected are the Time
130    // object and the ImageIcon object. The next step of the import algorithm is
131    // to check for their presence.
132    let (mut time_encountered, mut icon_encountered) = (false, false);
133
134    let mut aggregate_time_ms = 0;
135
136    // Seek to the first byte of the first segment
137    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                // Seek past the object declaration
152                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        // Skip to the segment name
200        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        // Read the segment time
209        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                // Since there is always a best segment when there is a best
218                // time in Llanfair, I assume that there will never be another
219                // Time object declaration before this data.
220                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        // Seek to the beginning of the next segment name
236        cursor = cursor.get(0x6..).context(ReadSegment)?;
237    }
238
239    Ok(run)
240}