1use 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#[derive(Debug, snafu::Snafu)]
23pub enum Error {
24 Xml {
26 source: XmlError,
28 },
29 ParseInt {
31 source: core::num::ParseIntError,
33 },
34 ParseFloat {
36 source: core::num::ParseFloatError,
38 },
39 ParseTime {
41 source: crate::timing::ParseError,
43 },
44 ParseDate,
46 InvalidComparisonName {
48 source: ComparisonError,
50 },
51 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
85pub type Result<T> = core::result::Result<T, Error>;
87
88#[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
429pub 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}