use crate::{
util::byte_parsing::{
big_endian::{strip_u16, strip_u32, strip_u64},
strip_slice, strip_u8,
},
RealTime, Run, Segment, Time, TimeSpan,
};
use core::{result::Result as StdResult, str};
#[cfg(feature = "std")]
use image::{codecs::png, ColorType, ImageBuffer, ImageEncoder, Rgba};
use snafu::{OptionExt, ResultExt};
#[derive(Debug, snafu::Snafu)]
#[snafu(context(suffix(false)))]
pub enum Error {
ReadHeader,
InvalidHeader,
DetermineLength,
SkipToGoal,
ReadGoal {
source: StringError,
},
SkipToTitle,
ReadTitle {
source: StringError,
},
ReadSegmentCount,
ReadSegment,
ReadBestSegment,
ReadIcon,
#[snafu(display("The dimensions {width}x{height} of a segment's icon are not valid."))]
InvalidIconDimensions {
width: u32,
height: u32,
},
ReadImageData,
ReadSegmentName {
source: StringError,
},
ReadSegmentTime,
}
#[derive(Debug, snafu::Snafu)]
#[snafu(context(suffix(false)))]
pub enum StringError {
ReadLength,
LengthOutOfBounds,
Validate,
}
pub type Result<T> = StdResult<T, Error>;
fn to_time(milliseconds: u64) -> Time {
if milliseconds == 0 {
Time::default()
} else {
RealTime(Some(TimeSpan::from_milliseconds(milliseconds as f64))).into()
}
}
fn read_string<'a>(cursor: &mut &'a [u8]) -> StdResult<&'a str, StringError> {
let str_length = strip_u16(cursor).context(ReadLength)? as usize;
let str_data = strip_slice(cursor, str_length).context(LengthOutOfBounds)?;
simdutf8::basic::from_utf8(str_data).ok().context(Validate)
}
pub fn parse(source: &[u8]) -> Result<Run> {
#[cfg(feature = "std")]
let mut buf = Vec::new();
const HEADER: &[u8; 30] = b"\xAC\xED\0\x05\x73\x72\0\x16org.fenix.llanfair.Run";
if !source.starts_with(HEADER) {
return Err(Error::InvalidHeader);
}
let mut run = Run::new();
let mut cursor = source.get(0xc5..).context(SkipToGoal)?;
run.set_category_name(read_string(&mut cursor).context(ReadGoal)?);
cursor = cursor.get(1..).context(SkipToTitle)?;
run.set_game_name(read_string(&mut cursor).context(ReadTitle)?);
cursor = cursor.get(0x6..).context(ReadSegmentCount)?;
let segment_count = strip_u32(&mut cursor).context(ReadSegmentCount)?;
let (mut time_encountered, mut icon_encountered) = (false, false);
let mut aggregate_time_ms = 0;
cursor = cursor.get(0x8F..).context(ReadSegment)?;
run.segments_mut().reserve(segment_count as usize);
for _ in 0..segment_count {
#[cfg(feature = "std")]
let mut icon = None;
let mut best_segment_ms = 0;
let id = strip_u8(&mut cursor).context(ReadSegment)?;
if id != 0x70 {
if !time_encountered {
time_encountered = true;
cursor = cursor.get(0x36..).context(ReadSegment)?;
} else {
cursor = cursor.get(0x5..).context(ReadSegment)?;
}
best_segment_ms = strip_u64(&mut cursor).context(ReadBestSegment)?;
}
let id = strip_u8(&mut cursor).context(ReadIcon)?;
if id != 0x70 {
let seek_offset_base = if !icon_encountered {
icon_encountered = true;
cursor = cursor.get(0xBC..).context(ReadIcon)?;
0x25
} else {
cursor = cursor.get(0x5..).context(ReadIcon)?;
0x18
};
let height = strip_u32(&mut cursor).context(ReadIcon)?;
let width = strip_u32(&mut cursor).context(ReadIcon)?;
cursor = cursor.get(seek_offset_base..).context(ReadIcon)?;
let len = (width as usize)
.checked_mul(height as usize)
.and_then(|b| b.checked_mul(4))
.context(InvalidIconDimensions { width, height })?;
if cursor.len() < len {
return Err(Error::InvalidIconDimensions { width, height });
}
let (_image_data, rem) = cursor.split_at(len);
cursor = rem;
#[cfg(feature = "std")]
if let Some(image) = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, _image_data) {
buf.clear();
if png::PngEncoder::new(&mut buf)
.write_image(image.as_ref(), width, height, ColorType::Rgba8)
.is_ok()
{
icon = Some(crate::settings::Image::new(&buf));
}
}
}
cursor = cursor.get(1..).context(ReadSegment)?;
let mut segment = Segment::new(read_string(&mut cursor).context(ReadSegmentName)?);
#[cfg(feature = "std")]
if let Some(icon) = icon {
segment.set_icon(icon);
}
let id = strip_u8(&mut cursor).context(ReadSegmentTime)?;
let segment_time_ms = match id {
0x71 => {
cursor = cursor.get(0x4..).context(ReadSegmentTime)?;
best_segment_ms
}
0x70 => 0,
_ => {
cursor = cursor.get(0x5..).context(ReadSegmentTime)?;
strip_u64(&mut cursor).context(ReadSegmentTime)?
}
};
if segment_time_ms != 0 {
aggregate_time_ms += segment_time_ms;
let split_time = to_time(aggregate_time_ms);
segment.set_personal_best_split_time(split_time);
}
segment.set_best_segment_time(to_time(best_segment_ms));
run.push_segment(segment);
cursor = cursor.get(0x6..).context(ReadSegment)?;
}
Ok(run)
}