livesplit_core/run/parser/
livesplit.rs

1//! Provides the parser for LiveSplit splits files.
2
3use super::super::ComparisonError;
4use crate::{
5    platform::prelude::*,
6    util::xml::{
7        helper::{
8            attribute, attribute_escaped_err, end_tag, optional_attribute_escaped_err,
9            parse_attributes, parse_base, parse_children, reencode_children, text,
10            text_as_escaped_string_err, text_parsed, Error as XmlError,
11        },
12        Reader,
13    },
14    AtomicDateTime, DateTime, Run, RunMetadata, Segment, Time, TimeSpan,
15};
16use alloc::borrow::Cow;
17use core::{mem::MaybeUninit, str};
18use time::{Date, PrimitiveDateTime};
19
20/// The Error type for splits files that couldn't be parsed by the LiveSplit
21/// Parser.
22#[derive(Debug, snafu::Snafu)]
23pub enum Error {
24    /// The underlying XML format couldn't be parsed.
25    Xml {
26        /// The underlying error.
27        source: XmlError,
28    },
29    /// Failed to parse an integer.
30    ParseInt {
31        /// The underlying error.
32        source: core::num::ParseIntError,
33    },
34    /// Failed to parse a floating point number.
35    ParseFloat {
36        /// The underlying error.
37        source: core::num::ParseFloatError,
38    },
39    /// Failed to parse a time.
40    ParseTime {
41        /// The underlying error.
42        source: crate::timing::ParseError,
43    },
44    /// Failed to parse a date.
45    ParseDate,
46    /// Parsed comparison has an invalid name.
47    InvalidComparisonName {
48        /// The underlying error.
49        source: ComparisonError,
50    },
51    /// Failed to parse a boolean.
52    ParseBool,
53}
54
55impl From<XmlError> for Error {
56    fn from(source: XmlError) -> Self {
57        Self::Xml { source }
58    }
59}
60
61impl From<core::num::ParseIntError> for Error {
62    fn from(source: core::num::ParseIntError) -> Self {
63        Self::ParseInt { source }
64    }
65}
66
67impl From<core::num::ParseFloatError> for Error {
68    fn from(source: core::num::ParseFloatError) -> Self {
69        Self::ParseFloat { source }
70    }
71}
72
73impl From<crate::timing::ParseError> for Error {
74    fn from(source: crate::timing::ParseError) -> Self {
75        Self::ParseTime { source }
76    }
77}
78
79impl From<ComparisonError> for Error {
80    fn from(source: ComparisonError) -> Self {
81        Self::InvalidComparisonName { source }
82    }
83}
84
85/// The Result type for the LiveSplit Parser.
86pub type Result<T> = core::result::Result<T, Error>;
87
88// FIXME: Generalized Type Ascription (GTA 6)
89#[inline]
90const fn type_hint<T>(v: Result<T>) -> Result<T> {
91    v
92}
93
94#[derive(Copy, Clone, PartialOrd, PartialEq, Ord, Eq)]
95struct Version(u32, u32, u32, u32);
96
97fn parse_version(version: &str) -> Result<Version> {
98    let splits = version.split('.');
99    let mut v = [1, 0, 0, 0];
100    for (d, s) in v.iter_mut().zip(splits) {
101        *d = s.parse()?;
102    }
103    Ok(Version(v[0], v[1], v[2], v[3]))
104}
105
106fn parse_date_time(text: &str) -> Result<DateTime> {
107    catch! {
108        let (month, rem) = text.split_once('/')?;
109        let (day, rem) = rem.split_once('/')?;
110        let (year, rem) = rem.split_once(' ')?;
111        let (hour, rem) = rem.split_once(':')?;
112        let (minute, second) = rem.split_once(':')?;
113        PrimitiveDateTime::new(
114            Date::from_calendar_date(
115                year.parse().ok()?,
116                month
117                    .parse::<u8>()
118                    .ok()?
119                    .try_into()
120                    .ok()?,
121                day.parse().ok()?,
122            )
123            .ok()?,
124            time::Time::from_hms(
125                hour.parse().ok()?,
126                minute.parse().ok()?,
127                second.parse().ok()?,
128            )
129            .ok()?,
130        )
131        .assume_utc()
132    }
133    .ok_or(Error::ParseDate)
134}
135
136fn image<F>(reader: &mut Reader<'_>, image_buf: &mut Vec<MaybeUninit<u8>>, f: F) -> Result<()>
137where
138    F: FnOnce(&[u8]),
139{
140    text_as_escaped_string_err(reader, |text| {
141        if text.len() >= 216 {
142            let src = &text.as_bytes()[212..];
143
144            image_buf.resize(
145                base64_simd::STANDARD.estimated_decoded_length(src.len()),
146                MaybeUninit::uninit(),
147            );
148
149            if let Ok(decoded) =
150                base64_simd::STANDARD.decode(src, base64_simd::Out::from_uninit_slice(image_buf))
151            {
152                f(&decoded[2..decoded.len() - 1]);
153                return Ok(());
154            }
155        }
156        f(&[]);
157        Ok(())
158    })
159}
160
161fn time_span<F>(reader: &mut Reader<'_>, f: F) -> Result<()>
162where
163    F: FnOnce(TimeSpan),
164{
165    text_as_escaped_string_err(reader, |text| {
166        let time_span = || -> Result<TimeSpan> {
167            if let Some((before_dot, after_dot)) = text.split_once('.') {
168                if after_dot.contains(':') {
169                    let days = TimeSpan::from_days(before_dot.parse()?);
170                    let time = after_dot.parse()?;
171                    return Ok(days + time);
172                }
173            }
174            text.parse().map_err(Into::into)
175        }()?;
176        f(time_span);
177        Ok(())
178    })
179}
180
181fn time_span_opt<F>(reader: &mut Reader<'_>, f: F) -> Result<()>
182where
183    F: FnOnce(Option<TimeSpan>),
184{
185    text_as_escaped_string_err(reader, |text| {
186        let time_span = || -> Result<Option<TimeSpan>> {
187            if text.is_empty() {
188                return Ok(None);
189            }
190            if let Some((before_dot, after_dot)) = text.split_once('.') {
191                if after_dot.contains(':') {
192                    let days = TimeSpan::from_days(before_dot.parse()?);
193                    let time = after_dot.parse()?;
194                    return Ok(Some(days + time));
195                }
196            }
197            Ok(Some(text.parse()?))
198        }()?;
199        f(time_span);
200        Ok(())
201    })
202}
203
204fn time<F>(reader: &mut Reader<'_>, f: F) -> Result<()>
205where
206    F: FnOnce(Time),
207{
208    let mut time = Time::new();
209
210    parse_children(reader, |reader, tag, _| {
211        if tag.name() == "RealTime" {
212            time_span_opt(reader, |t| time.real_time = t)
213        } else if tag.name() == "GameTime" {
214            time_span_opt(reader, |t| time.game_time = t)
215        } else {
216            end_tag(reader)
217        }
218    })?;
219
220    f(time);
221
222    Ok(())
223}
224
225fn time_old<F>(reader: &mut Reader<'_>, f: F) -> Result<()>
226where
227    F: FnOnce(Time),
228{
229    time_span_opt(reader, |t| f(Time::new().with_real_time(t)))
230}
231
232fn parse_bool(value: &str) -> Result<bool> {
233    match value {
234        "True" => Ok(true),
235        "False" => Ok(false),
236        _ => Err(Error::ParseBool),
237    }
238}
239
240fn parse_metadata(
241    version: Version,
242    reader: &mut Reader<'_>,
243    metadata: &mut RunMetadata,
244) -> Result<()> {
245    if version >= Version(1, 6, 0, 0) {
246        parse_children(reader, |reader, tag, attributes| match tag.name() {
247            "Run" => {
248                type_hint(attribute(attributes, "id", |t| metadata.set_run_id(t)))?;
249                end_tag(reader)
250            }
251            "Platform" => {
252                type_hint(attribute_escaped_err(attributes, "usesEmulator", |t| {
253                    metadata.set_emulator_usage(parse_bool(t)?);
254                    Ok(())
255                }))?;
256                text(reader, |t| metadata.set_platform_name(t))
257            }
258            "Region" => text(reader, |t| metadata.set_region_name(t)),
259            "Variables" | "SpeedrunComVariables" => {
260                parse_children(reader, |reader, _, attributes| {
261                    let mut name = Cow::Borrowed("");
262                    let mut value = Cow::Borrowed("");
263                    type_hint(attribute(attributes, "name", |t| name = t))?;
264                    type_hint(text(reader, |t| value = t))?;
265                    metadata.set_speedrun_com_variable(name, value);
266                    Ok(())
267                })
268            }
269            "CustomVariables" => parse_children(reader, |reader, _, attributes| {
270                let mut name = Cow::Borrowed("");
271                type_hint(attribute(attributes, "name", |t| name = t))?;
272                let var = metadata.custom_variable_mut(name).permanent();
273                type_hint(text(reader, |t| var.set_value(t)))?;
274                Ok(())
275            }),
276            _ => end_tag(reader),
277        })
278    } else {
279        end_tag(reader)
280    }
281}
282
283fn parse_segment(
284    version: Version,
285    reader: &mut Reader<'_>,
286    image_buf: &mut Vec<MaybeUninit<u8>>,
287    run: &mut Run,
288) -> Result<Segment> {
289    let mut segment = Segment::new("");
290
291    parse_children(reader, |reader, tag, _| match tag.name() {
292        "Name" => text(reader, |t| segment.set_name(t)),
293        "Icon" => image(reader, image_buf, |i| segment.set_icon(i)),
294        "SplitTimes" => {
295            if version >= Version(1, 3, 0, 0) {
296                parse_children(reader, |reader, tag, attributes| {
297                    if tag.name() == "SplitTime" {
298                        let mut comparison = Cow::Borrowed("");
299                        type_hint(attribute(attributes, "name", |t| comparison = t))?;
300                        if version >= Version(1, 4, 1, 0) {
301                            time(reader, |t| *segment.comparison_mut(&comparison) = t)?;
302                        } else {
303                            time_old(reader, |t| *segment.comparison_mut(&comparison) = t)?;
304                        }
305                        if let Err(ComparisonError::NameStartsWithRace) =
306                            run.add_custom_comparison(comparison)
307                        {
308                            return Err(ComparisonError::NameStartsWithRace.into());
309                        }
310                        Ok(())
311                    } else {
312                        end_tag(reader)
313                    }
314                })
315            } else {
316                end_tag(reader)
317            }
318        }
319        "PersonalBestSplitTime" => {
320            if version < Version(1, 3, 0, 0) {
321                time_old(reader, |t| segment.set_personal_best_split_time(t))
322            } else {
323                end_tag(reader)
324            }
325        }
326        "BestSegmentTime" => {
327            if version >= Version(1, 4, 1, 0) {
328                time(reader, |t| segment.set_best_segment_time(t))
329            } else {
330                time_old(reader, |t| segment.set_best_segment_time(t))
331            }
332        }
333        "SegmentHistory" => parse_children(reader, |reader, _, attributes| {
334            let mut index = 0;
335            type_hint(attribute_escaped_err(attributes, "id", |t| {
336                index = t.parse()?;
337                Ok(())
338            }))?;
339            if version >= Version(1, 4, 1, 0) {
340                time(reader, |t| segment.segment_history_mut().insert(index, t))
341            } else {
342                time_old(reader, |t| segment.segment_history_mut().insert(index, t))
343            }
344        }),
345        _ => end_tag(reader),
346    })?;
347
348    Ok(segment)
349}
350
351fn parse_run_history(version: Version, reader: &mut Reader<'_>, run: &mut Run) -> Result<()> {
352    if version >= Version(1, 5, 0, 0) {
353        end_tag(reader)
354    } else if version >= Version(1, 4, 1, 0) {
355        parse_children(reader, |reader, _, attributes| {
356            let mut index = 0;
357            type_hint(attribute_escaped_err(attributes, "id", |t| {
358                index = t.parse()?;
359                Ok(())
360            }))?;
361            time(reader, |time| {
362                run.add_attempt_with_index(time, index, None, None, None)
363            })
364        })
365    } else {
366        parse_children(reader, |reader, _, attributes| {
367            let mut index = 0;
368            type_hint(attribute_escaped_err(attributes, "id", |t| {
369                index = t.parse()?;
370                Ok(())
371            }))?;
372            time_old(reader, |time| {
373                run.add_attempt_with_index(time, index, None, None, None)
374            })
375        })
376    }
377}
378
379fn parse_attempt_history(version: Version, reader: &mut Reader<'_>, run: &mut Run) -> Result<()> {
380    if version >= Version(1, 5, 0, 0) {
381        parse_children(reader, |reader, _, attributes| {
382            let mut time = Time::new();
383            let mut pause_time = None;
384            let mut index = None;
385            let (mut started, mut started_synced) = (None, false);
386            let (mut ended, mut ended_synced) = (None, false);
387
388            type_hint(parse_attributes(attributes, |k, v| {
389                match k {
390                    "id" => index = Some(v.escaped().parse()?),
391                    "started" => started = Some(parse_date_time(v.escaped())?),
392                    "isStartedSynced" => started_synced = parse_bool(v.escaped())?,
393                    "ended" => ended = Some(parse_date_time(v.escaped())?),
394                    "isEndedSynced" => ended_synced = parse_bool(v.escaped())?,
395                    _ => {}
396                }
397                Ok(true)
398            }))?;
399
400            let index = index.ok_or(Error::Xml {
401                source: XmlError::AttributeNotFound,
402            })?;
403
404            parse_children(reader, |reader, tag, _| match tag.name() {
405                "RealTime" => time_span_opt(reader, |t| time.real_time = t),
406                "GameTime" => time_span_opt(reader, |t| time.game_time = t),
407                "PauseTime" => time_span_opt(reader, |t| pause_time = t),
408                _ => end_tag(reader),
409            })?;
410
411            let started = started.map(|t| AtomicDateTime::new(t, started_synced));
412            let ended = if version <= Version(1, 7, 0, 0)
413                && catch! { ended? < started?.time }.unwrap_or(false)
414            {
415                None
416            } else {
417                ended.map(|t| AtomicDateTime::new(t, ended_synced))
418            };
419
420            run.add_attempt_with_index(time, index, started, ended, pause_time);
421
422            Ok(())
423        })
424    } else {
425        end_tag(reader)
426    }
427}
428
429/// Attempts to parse a LiveSplit splits file.
430pub fn parse(source: &str) -> Result<Run> {
431    let mut reader = Reader::new(source);
432
433    let mut image_buf = Vec::new();
434
435    let mut run = Run::new();
436
437    let mut required_flags = 0u8;
438
439    parse_base(&mut reader, "Run", |reader, attributes| {
440        let mut version = Version(1, 0, 0, 0);
441        type_hint(optional_attribute_escaped_err(attributes, "version", |t| {
442            version = parse_version(t)?;
443            Ok(())
444        }))?;
445
446        parse_children(reader, |reader, tag, _| match tag.name() {
447            "GameIcon" => {
448                required_flags |= 1;
449                image(reader, &mut image_buf, |i| run.set_game_icon(i))
450            }
451            "GameName" => {
452                required_flags |= 1 << 1;
453                text(reader, |t| run.set_game_name(t))
454            }
455            "CategoryName" => {
456                required_flags |= 1 << 2;
457                text(reader, |t| run.set_category_name(t))
458            }
459            "Offset" => {
460                required_flags |= 1 << 3;
461                time_span(reader, |t| run.set_offset(t))
462            }
463            "AttemptCount" => {
464                required_flags |= 1 << 4;
465                text_parsed(reader, |t| run.set_attempt_count(t))
466            }
467            "AttemptHistory" => parse_attempt_history(version, reader, &mut run),
468            "RunHistory" => parse_run_history(version, reader, &mut run),
469            "Metadata" => parse_metadata(version, reader, run.metadata_mut()),
470            "Segments" => {
471                required_flags |= 1 << 5;
472                parse_children(reader, |reader, tag, _| {
473                    if tag.name() == "Segment" {
474                        let segment = parse_segment(version, reader, &mut image_buf, &mut run)?;
475                        run.push_segment(segment);
476                        Ok(())
477                    } else {
478                        end_tag(reader)
479                    }
480                })
481            }
482            "AutoSplitterSettings" => {
483                let settings = run.auto_splitter_settings_mut();
484                reencode_children(reader, settings).map_err(Into::into)
485            }
486            _ => end_tag(reader),
487        })
488    })?;
489
490    if required_flags != (1 << 6) - 1 {
491        return Err(Error::Xml {
492            source: XmlError::ElementNotFound,
493        });
494    }
495
496    Ok(run)
497}