use glam::{Mat4, Vec2, Vec3, Vec4};
use ltk_primitives::Color;
use crate::{
property::values,
property::values::{Embedded, UnorderedContainer},
stream::layout::{Cursor, Numbering},
traits::PropertyExt as _,
Error, PropertyValueEnum,
};
fn cursor(bytes: &[u8]) -> Cursor<'_> {
Cursor::new(bytes, Numbering::Current)
}
fn body(value: &PropertyValueEnum) -> Vec<u8> {
let mut cursor = std::io::Cursor::new(Vec::new());
value.to_writer(&mut cursor).expect("the value writes");
cursor.into_inner()
}
fn one_of_each() -> Vec<PropertyValueEnum> {
let object = values::Struct {
class_hash: 0x1234u32.into(),
properties: [
(0x1111u32.into(), values::I32::new(42).into()),
(0x2222u32.into(), values::String::from("hello").into()),
]
.into_iter()
.collect(),
};
vec![
values::None::default().into(),
values::Bool::new(true).into(),
values::BitBool::new(true).into(),
values::I8::new(-1).into(),
values::U8::new(1).into(),
values::I16::new(-2).into(),
values::U16::new(2).into(),
values::I32::new(-3).into(),
values::U32::new(3).into(),
values::I64::new(-4).into(),
values::U64::new(4).into(),
values::F32::new(1.5).into(),
values::Vector2::new(Vec2::ONE).into(),
values::Vector3::new(Vec3::ONE).into(),
values::Vector4::new(Vec4::ONE).into(),
values::Matrix44::new(Mat4::IDENTITY).into(),
values::Color::new(Color::new(1, 2, 3, 4)).into(),
values::String::from("a string").into(),
values::Hash::new(0xABCDu32).into(),
values::WadChunkLink::new(0xDEAD_BEEFu64).into(),
values::ObjectLink::new(0x4444u32).into(),
object.clone().into(),
values::Struct::default().into(),
Embedded(object).into(),
values::Container::from(vec![values::I32::new(1), values::I32::new(2)]).into(),
values::Container::from(vec![
values::String::from("one"),
values::String::from("two"),
])
.into(),
UnorderedContainer(values::Container::from(vec![values::U8::new(9)])).into(),
values::Optional::from(values::F32::new(2.5)).into(),
values::Optional::empty(crate::PropertyKind::F32)
.expect("F32 nests")
.into(),
values::Map::new(
crate::PropertyKind::U32,
crate::PropertyKind::String,
vec![(
values::U32::new(7).into(),
values::String::from("seven").into(),
)],
)
.expect("a valid map")
.into(),
]
}
#[test]
fn skip_and_walk_distances_match_the_written_size() {
for value in one_of_each() {
let bytes = body(&value);
assert_eq!(
bytes.len(),
value.size_no_header(),
"{:?}: the writer and PropertyExt::size disagree",
value.kind()
);
let mut cur = cursor(&bytes);
cur.skip_value(value.kind()).expect("the value skips");
assert_eq!(
cur.position(),
bytes.len(),
"{:?}: skip distance is not the serialized size",
value.kind()
);
let mut cur = cursor(&bytes);
cur.walk_value(value.kind()).expect("the value walks");
assert_eq!(
cur.position(),
bytes.len(),
"{:?}: walk distance is not the serialized size",
value.kind()
);
let mut cur = cursor(&bytes);
assert_eq!(
cur.take_value(value.kind()).expect("the value is taken"),
bytes,
"{:?}: the taken bytes are not the value's own",
value.kind()
);
assert_eq!(cur.remaining(), 0);
}
}
#[test]
fn shapes_come_from_the_header_bytes() {
use crate::PropertyKind as K;
for value in one_of_each() {
let bytes = body(&value);
let cur = cursor(&bytes);
assert_eq!(
cur.value_shape(value.kind()).expect("the shape reads"),
crate::path::ValueShape::of(&value),
"{:?}: the declared shape and ValueShape::of disagree",
value.kind()
);
assert_eq!(cur.position(), 0, "reading a shape moved the cursor");
}
let map: PropertyValueEnum = values::Map::empty(K::Hash, K::I32).expect("valid").into();
let shape = cursor(&body(&map)).value_shape(K::Map).expect("reads");
assert_eq!(shape.key_kind, Some(K::Hash));
assert_eq!(shape.item_kind, Some(K::I32));
}
#[test]
fn cursor_codecs_agree_with_the_reader_codecs() {
use crate::{stream::owned, traits::ReadProperty};
macro_rules! same_leaf {
($($owned:expr),* $(,)?) => {$({
let owned = $owned;
let value: PropertyValueEnum = owned.clone().into();
let bytes = body(&value);
let via_reader = ReadProperty::from_reader(
&mut std::io::Cursor::new(&bytes[..]),
false,
)
.expect("the reader codec reads");
assert_eq!(owned, via_reader, "the reader codec disagrees with the writer");
let via_cursor = owned::read_value(&mut cursor(&bytes), value.kind())
.expect("the cursor codec reads");
assert_eq!(value, via_cursor, "the two leaf codecs disagree");
})*};
}
same_leaf!(
values::Bool::new(true),
values::BitBool::new(true),
values::I8::new(-1),
values::U8::new(200),
values::I16::new(-300),
values::U16::new(40_000),
values::I32::new(-70_000),
values::U32::new(3_000_000_000),
values::I64::new(-5_000_000_000),
values::U64::new(18_000_000_000_000_000_000),
values::F32::new(-1.25),
values::Vector2::new(Vec2::new(1.0, 2.0)),
values::Vector3::new(Vec3::new(1.0, 2.0, 3.0)),
values::Vector4::new(Vec4::new(1.0, 2.0, 3.0, 4.0)),
values::Matrix44::new(Mat4::from_scale(Vec3::new(1.0, 2.0, 3.0))),
values::Color::new(Color::new(1, 2, 3, 4)),
values::Hash::new(0xABCDu32),
values::WadChunkLink::new(0xDEAD_BEEF_0000_0001u64),
values::ObjectLink::new(0x4444u32),
values::String::from("héllo"),
);
}
#[test]
fn an_object_walks_to_its_declared_size() {
let object = crate::BinObject::builder(0x1111u32, 0x2222u32)
.property(0x0001u32, values::I32::new(42))
.property(0x0002u32, values::String::from("hello"))
.build();
let mut written = std::io::Cursor::new(Vec::new());
object.to_writer(&mut written).expect("the object writes");
let bytes = written.into_inner();
let mut cur = cursor(&bytes);
cur.walk_object().expect("the object walks");
assert_eq!(cur.position(), bytes.len());
let mut lying = bytes.clone();
let declared = u32::from_le_bytes(lying[..4].try_into().expect("4 bytes"));
lying[..4].copy_from_slice(&(declared + 2).to_le_bytes());
lying.extend_from_slice(&[0; 2]);
let error = cursor(&lying)
.walk_object()
.expect_err("the counts disagree with the declared size");
assert!(
matches!(error, Error::InvalidSize(d, c) if d == u64::from(declared) + 2 && c == u64::from(declared)),
"unexpected error: {error}"
);
}
#[test]
fn a_container_that_declares_a_container_item_is_refused_by_every_header_read() {
use crate::PropertyKind as K;
let list: PropertyValueEnum = values::Container::from(vec![values::I32::new(1)]).into();
let mut bytes = body(&list);
bytes[0] = u8::from(K::Container);
let error = cursor(&bytes)
.walk_value(K::Container)
.expect_err("a container item kind is refused");
assert!(
matches!(error, Error::InvalidNesting(K::Container)),
"unexpected error: {error}"
);
let error = cursor(&bytes)
.value_shape(K::Container)
.expect_err("the shape peek reads the same header");
assert!(
matches!(error, Error::InvalidNesting(K::Container)),
"unexpected error: {error}"
);
}
#[test]
fn a_map_that_declares_a_key_kind_no_map_is_keyed_by_is_refused_by_every_header_read() {
use crate::PropertyKind as K;
let map: PropertyValueEnum = values::Map::new(
K::Hash,
K::I32,
vec![(
values::Hash::new(0x1111u32).into(),
values::I32::new(1).into(),
)],
)
.expect("the map builds")
.into();
let mut bytes = body(&map);
bytes[0] = u8::from(K::Struct);
let error = cursor(&bytes)
.walk_value(K::Map)
.expect_err("a key kind no map is keyed by is refused");
assert!(
matches!(error, Error::InvalidKeyType(K::Struct)),
"unexpected error: {error}"
);
let error = cursor(&bytes)
.value_shape(K::Map)
.expect_err("the shape peek reads the same header");
assert!(
matches!(error, Error::InvalidKeyType(K::Struct)),
"unexpected error: {error}"
);
}
#[test]
fn a_lying_size_is_an_invalid_size_error() {
let list: PropertyValueEnum =
values::Container::from(vec![values::I32::new(1), values::I32::new(2)]).into();
let mut bytes = body(&list);
bytes[1..5].copy_from_slice(&20u32.to_le_bytes());
bytes.extend_from_slice(&[0xFF; 8]);
let error = cursor(&bytes)
.walk_value(crate::PropertyKind::Container)
.expect_err("the counts disagree with the declared size");
assert!(
matches!(error, Error::InvalidSize(20, 12)),
"unexpected error: {error}"
);
let mut cur = cursor(&bytes);
cur.skip_value(crate::PropertyKind::Container)
.expect("skips");
assert_eq!(cur.position(), bytes.len());
}
#[test]
fn leaf_codecs_decode_what_the_writer_wrote() {
let value: PropertyValueEnum = values::String::from("héllo").into();
let bytes = body(&value);
let mut cur = cursor(&bytes);
assert_eq!(cur.str_u16().expect("valid UTF-8"), "héllo");
assert_eq!(cur.remaining(), 0);
let mut cur = cursor(&bytes);
assert!(matches!(cur.take(64), Err(Error::IOError(_))));
assert_eq!(cur.position(), 0, "a failed take does not advance");
let mut cur = cursor(&bytes);
assert!(matches!(cur.take(usize::MAX), Err(Error::IOError(_))));
}
#[test]
fn a_cursor_carries_the_numbering_its_bytes_use() {
let bytes = [19u8];
let mut current = cursor(&bytes);
assert_eq!(current.numbering(), Numbering::Current);
assert!(matches!(
current.kind(),
Err(Error::InvalidPropertyTypePrimitive(_))
));
let mut legacy = Cursor::new(&bytes, Numbering::Legacy);
assert_eq!(
legacy.kind().expect("19 is legacy Struct"),
crate::PropertyKind::Struct
);
}