// SPDX-FileCopyrightText: 2024 Joshua Goins // SPDX-License-Identifier: GPL-3.0-or-later #![allow(unused_variables)] // just binrw things with br(temp) use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use crate::common_file_operations::{read_bool_from, write_bool_as, write_string}; use crate::{ByteBuffer, ByteSpan}; use binrw::{BinRead, BinReaderExt, BinWrite, binread}; use binrw::{Endian, Error, binrw}; mod aetheryte; pub use aetheryte::AetheryteInstanceObject; mod bg; pub use bg::BGInstanceObject; pub use bg::ModelCollisionType; mod common; pub use common::Color; pub use common::ColorHDRI; pub use common::Transformation; mod env_set; pub use env_set::EnvSetInstanceObject; pub use env_set::EnvSetShape; mod exit_range; pub use exit_range::ExitRangeInstanceObject; pub use exit_range::ExitType; mod light; pub use light::LightInstanceObject; pub use light::LightType; pub use light::PointLightType; mod npc; pub use npc::BNPCInstanceObject; pub use npc::ENPCInstanceObject; pub use npc::GameInstanceObject; pub use npc::NPCInstanceObject; mod pop; pub use pop::PopRangeInstanceObject; pub use pop::PopType; mod position_marker; pub use position_marker::PositionMarkerInstanceObject; pub use position_marker::PositionMarkerType; mod shared_group; pub use shared_group::ColourState; pub use shared_group::DoorState; pub use shared_group::RotationState; pub use shared_group::SharedGroupInstance; pub use shared_group::TransformState; mod sound; pub use sound::SoundInstanceObject; mod trigger_box; pub use trigger_box::TriggerBoxInstanceObject; pub use trigger_box::TriggerBoxShape; // From https://github.com/NotAdam/Lumina/tree/40dab50183eb7ddc28344378baccc2d63ae71d35/src/Lumina/Data/Parsing/Layer // Also see https://github.com/aers/FFXIVClientStructs/blob/6b62122cae38bfbc016bf697bef75f80f37abac1/FFXIVClientStructs/FFXIV/Client/LayoutEngine/ILayoutInstance.cs /// "LGB1" pub const LGB1_ID: u32 = u32::from_le_bytes(*b"LGB1"); /// "LGP1" pub const LGP1_ID: u32 = u32::from_le_bytes(*b"LGP1"); /// A string that exists in a different location in the file, usually a heap with a bunch of other strings. #[binrw] #[br(import(string_heap: &StringHeap), stream = r)] #[bw(import(string_heap: &mut StringHeap))] #[derive(Clone, Debug)] pub struct HeapString { #[br(temp)] // TODO: this cast is stupid #[bw(calc = string_heap.get_free_offset_string(value) as u32)] pub offset: u32, #[br(calc = string_heap.read_string(r, offset,))] #[bw(ignore)] pub value: String, } #[derive(Debug)] pub struct StringHeap { pub pos: u64, pub bytes: Vec, pub free_pos: u64, } impl StringHeap { pub fn from(pos: u64) -> Self { Self { pos, bytes: Vec::new(), free_pos: 0, // unused, so it doesn't matter } } pub fn get_free_offset_args(&mut self, obj: &T) -> i32 where T: for<'a> BinWrite = (&'a mut StringHeap,)> + std::fmt::Debug, { // figure out size of it let mut buffer = ByteBuffer::new(); { let mut cursor = Cursor::new(&mut buffer); obj.write_le_args(&mut cursor, (self,)).unwrap(); } self.bytes.append(&mut buffer); let old_pos = self.free_pos; self.free_pos += buffer.len() as u64; old_pos as i32 } pub fn get_free_offset(&mut self, obj: &T) -> i32 where T: for<'a> BinWrite = ()> + std::fmt::Debug, { // figure out size of it let mut buffer = ByteBuffer::new(); { let mut cursor = Cursor::new(&mut buffer); obj.write_le(&mut cursor).unwrap(); } self.bytes.append(&mut buffer); let old_pos = self.free_pos; self.free_pos += buffer.len() as u64; old_pos as i32 } pub fn get_free_offset_string(&mut self, str: &String) -> i32 { let bytes = write_string(str); self.get_free_offset(&bytes) } pub fn read(&self, reader: &mut R, offset: i32) -> T where R: Read + Seek, T: for<'a> BinRead = ()>, { let old_pos = reader.stream_position().unwrap(); reader .seek(SeekFrom::Start((self.pos as i32 + offset) as u64)) .unwrap(); let obj = reader.read_le::().unwrap(); reader.seek(SeekFrom::Start(old_pos)).unwrap(); obj } pub fn read_args(&self, reader: &mut R, offset: i32) -> T where R: Read + Seek, T: for<'a> BinRead = (&'a StringHeap,)>, { let old_pos = reader.stream_position().unwrap(); reader .seek(SeekFrom::Start((self.pos as i32 + offset) as u64)) .unwrap(); let obj = reader.read_le_args::((self,)).unwrap(); reader.seek(SeekFrom::Start(old_pos)).unwrap(); obj } pub fn read_string(&self, reader: &mut R, offset: u32) -> String where R: Read + Seek, { let offset = self.pos + offset as u64; let mut string = String::new(); let old_pos = reader.stream_position().unwrap(); reader.seek(SeekFrom::Start(offset)).unwrap(); let mut next_char = reader.read_le::().unwrap() as char; while next_char != '\0' { string.push(next_char); next_char = reader.read_le::().unwrap() as char; } reader.seek(SeekFrom::Start(old_pos)).unwrap(); string } } impl BinWrite for StringHeap { type Args<'a> = (); fn write_options( &self, writer: &mut W, endian: Endian, (): Self::Args<'_>, ) -> Result<(), Error> { self.bytes.write_options(writer, endian, ())?; Ok(()) } } // TODO: convert these all to magic #[binrw] #[brw(repr = i32)] #[repr(i32)] #[derive(Debug, PartialEq)] pub enum LayerEntryType { AssetNone = 00, BG = 0x1, Attribute = 0x2, LayLight = 0x3, Vfx = 0x4, PositionMarker = 0x5, SharedGroup = 0x6, Sound = 0x7, EventNPC = 0x8, BattleNPC = 0x9, RoutePath = 0xA, Character = 0xB, Aetheryte = 0xC, EnvSet = 0xD, Gathering = 0xE, HelperObject = 0xF, Treasure = 0x10, Clip = 0x11, ClipCtrlPoint = 0x12, ClipCamera = 0x13, ClipLight = 0x14, ClipReserve00 = 0x15, ClipReserve01 = 0x16, ClipReserve02 = 0x17, ClipReserve03 = 0x18, ClipReserve04 = 0x19, ClipReserve05 = 0x1A, ClipReserve06 = 0x1B, ClipReserve07 = 0x1C, ClipReserve08 = 0x1D, ClipReserve09 = 0x1E, ClipReserve10 = 0x1F, ClipReserve11 = 0x20, ClipReserve12 = 0x21, ClipReserve13 = 0x22, ClipReserve14 = 0x23, CutAssetOnlySelectable = 0x24, Player = 0x25, Monster = 0x26, Weapon = 0x27, PopRange = 0x28, /// Zone Transitions (the visible part is probably LineVFX?) ExitRange = 0x29, Lvb = 0x2A, MapRange = 0x2B, NaviMeshRange = 0x2C, EventObject = 0x2D, DemiHuman = 0x2E, EnvLocation = 0x2F, ControlPoint = 0x30, EventRange = 0x31, RestBonusRange = 0x32, QuestMarker = 0x33, Timeline = 0x34, ObjectBehaviorSet = 0x35, Movie = 0x36, ScenarioExd = 0x37, ScenarioText = 0x38, CollisionBox = 0x39, DoorRange = 0x3A, LineVFX = 0x3B, SoundEnvSet = 0x3C, CutActionTimeline = 0x3D, CharaScene = 0x3E, CutAction = 0x3F, EquipPreset = 0x40, ClientPath = 0x41, ServerPath = 0x42, GimmickRange = 0x43, TargetMarker = 0x44, ChairMarker = 0x45, ClickableRange = 0x46, PrefetchRange = 0x47, FateRange = 0x48, PartyMember = 0x49, KeepRange = 0x4A, SphereCastRange = 0x4B, IndoorObject = 0x4C, OutdoorObject = 0x4D, EditGroup = 0x4E, StableChocobo = 0x4F, MaxAssetType = 0x50, Unk1 = 90, } #[binread] #[derive(Debug)] #[br(import(magic: &LayerEntryType, string_heap: &StringHeap))] pub enum LayerEntryData { #[br(pre_assert(*magic == LayerEntryType::BG))] BG(#[br(args(string_heap))] BGInstanceObject), #[br(pre_assert(*magic == LayerEntryType::LayLight))] LayLight(LightInstanceObject), #[br(pre_assert(*magic == LayerEntryType::Vfx))] Vfx(VFXInstanceObject), #[br(pre_assert(*magic == LayerEntryType::PositionMarker))] PositionMarker(PositionMarkerInstanceObject), #[br(pre_assert(*magic == LayerEntryType::SharedGroup))] SharedGroup(SharedGroupInstance), #[br(pre_assert(*magic == LayerEntryType::Sound))] Sound(SoundInstanceObject), #[br(pre_assert(*magic == LayerEntryType::EventNPC))] EventNPC(ENPCInstanceObject), #[br(pre_assert(*magic == LayerEntryType::BattleNPC))] BattleNPC(BNPCInstanceObject), #[br(pre_assert(*magic == LayerEntryType::Aetheryte))] Aetheryte(AetheryteInstanceObject), #[br(pre_assert(*magic == LayerEntryType::EnvSet))] EnvSet(EnvSetInstanceObject), #[br(pre_assert(*magic == LayerEntryType::Gathering))] Gathering(GatheringInstanceObject), #[br(pre_assert(*magic == LayerEntryType::Treasure))] Treasure(TreasureInstanceObject), #[br(pre_assert(*magic == LayerEntryType::PopRange))] PopRange(PopRangeInstanceObject), #[br(pre_assert(*magic == LayerEntryType::ExitRange))] ExitRange(ExitRangeInstanceObject), #[br(pre_assert(*magic == LayerEntryType::MapRange))] MapRange(MapRangeInstanceObject), #[br(pre_assert(*magic == LayerEntryType::EventObject))] EventObject(EventInstanceObject), #[br(pre_assert(*magic == LayerEntryType::EnvLocation))] EnvLocation(EnvLocationObject), #[br(pre_assert(*magic == LayerEntryType::EventRange))] EventRange(EventRangeInstanceObject), #[br(pre_assert(*magic == LayerEntryType::QuestMarker))] QuestMarker(QuestMarkerInstanceObject), #[br(pre_assert(*magic == LayerEntryType::CollisionBox))] CollisionBox(CollisionBoxInstanceObject), #[br(pre_assert(*magic == LayerEntryType::LineVFX))] LineVFX(LineVFXInstanceObject), #[br(pre_assert(*magic == LayerEntryType::ClientPath))] ClientPath(ClientPathInstanceObject), #[br(pre_assert(*magic == LayerEntryType::ServerPath))] ServerPath(ServerPathInstanceObject), #[br(pre_assert(*magic == LayerEntryType::GimmickRange))] GimmickRange(GimmickRangeInstanceObject), #[br(pre_assert(*magic == LayerEntryType::TargetMarker))] TargetMarker(TargetMarkerInstanceObject), #[br(pre_assert(*magic == LayerEntryType::ChairMarker))] ChairMarker(ChairMarkerInstanceObject), #[br(pre_assert(*magic == LayerEntryType::PrefetchRange))] PrefetchRange(PrefetchRangeInstanceObject), #[br(pre_assert(*magic == LayerEntryType::FateRange))] FateRange(FateRangeInstanceObject), #[br(pre_assert(*magic == LayerEntryType::Unk1))] Unk1(), } #[binread] #[derive(Debug)] #[br(little)] pub struct VFXInstanceObject { pub asset_path_offset: u32, #[brw(pad_after = 4)] // padding pub soft_particle_fade_range: f32, pub color: Color, #[br(map = read_bool_from::)] pub auto_play: bool, #[br(map = read_bool_from::)] #[brw(pad_after = 2)] // padding pub no_far_clip: bool, pub fade_near_start: f32, pub fade_near_end: f32, pub fade_far_start: f32, pub fade_far_end: f32, pub z_correct: f32, } #[binread] #[derive(Debug)] #[br(little)] pub struct GatheringInstanceObject { #[brw(pad_after = 4)] // padding pub gathering_point_id: u32, } #[binread] #[derive(Debug)] #[br(little)] pub struct TreasureInstanceObject { #[brw(pad_after = 11)] // padding pub nonpop_init_zone: u8, } // Unimplemented because I haven't needed it yet: #[binread] #[derive(Debug)] #[br(little)] pub struct MapRangeInstanceObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct EventInstanceObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct EnvLocationObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct EventRangeInstanceObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct QuestMarkerInstanceObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct CollisionBoxInstanceObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct LineVFXInstanceObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct ClientPathInstanceObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct ServerPathInstanceObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct GimmickRangeInstanceObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct TargetMarkerInstanceObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct ChairMarkerInstanceObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct PrefetchRangeInstanceObject {} #[binread] #[derive(Debug)] #[br(little)] pub struct FateRangeInstanceObject {} #[binrw] #[brw(repr = i32)] #[derive(Debug, PartialEq)] enum LayerSetReferencedType { All = 0x0, Include = 0x1, Exclude = 0x2, Undetermined = 0x3, } #[binrw] #[derive(Debug)] #[brw(little)] #[br(import(data_heap: &StringHeap, string_heap: &StringHeap), stream = r)] #[bw(import(data_heap: &mut StringHeap, string_heap: &mut StringHeap))] #[allow(dead_code)] // most of the fields are unused at the moment pub struct LayerHeader { pub layer_id: u32, #[brw(args(string_heap))] pub name: HeapString, pub instance_object_offset: i32, pub instance_object_count: i32, #[br(map = read_bool_from::)] #[bw(map = write_bool_as::)] pub tool_mode_visible: bool, #[br(map = read_bool_from::)] #[bw(map = write_bool_as::)] pub tool_mode_read_only: bool, #[br(map = read_bool_from::)] #[bw(map = write_bool_as::)] pub is_bush_layer: bool, #[br(map = read_bool_from::)] #[bw(map = write_bool_as::)] pub ps3_visible: bool, #[br(temp)] #[bw(calc = data_heap.get_free_offset_args(&layer_set_referenced_list))] pub layer_set_referenced_list_offset: i32, #[br(calc = data_heap.read_args(r, layer_set_referenced_list_offset))] #[bw(ignore)] pub layer_set_referenced_list: LayerSetReferencedList, pub festival_id: u16, pub festival_phase_id: u16, pub is_temporary: u8, pub is_housing: u8, pub version_mask: u16, #[brw(pad_before = 4)] pub ob_set_referenced_list: i32, pub ob_set_referenced_list_count: i32, pub ob_set_enable_referenced_list: i32, pub ob_set_enable_referenced_list_count: i32, } #[binrw] #[derive(Debug)] #[brw(little)] #[allow(dead_code)] // most of the fields are unused at the moment pub struct LayerSetReferenced { pub layer_set_id: u32, } #[binrw] #[derive(Debug)] #[brw(little)] #[br(import(data_heap: &StringHeap), stream = r)] #[bw(import(data_heap: &mut StringHeap))] pub struct LayerSetReferencedList { referenced_type: LayerSetReferencedType, #[br(temp)] #[bw(calc = data_heap.get_free_offset(&layer_sets))] layer_set_offset: i32, #[bw(calc = layer_sets.len() as i32)] pub layer_set_count: i32, #[br(count = layer_set_count)] #[bw(ignore)] pub layer_sets: Vec, } #[binread] #[derive(Debug)] #[br(little)] #[allow(dead_code)] // most of the fields are unused at the moment struct OBSetReferenced { asset_type: LayerEntryType, instance_id: u32, ob_set_asset_path_offset: u32, } #[binread] #[derive(Debug)] #[br(little)] #[allow(dead_code)] // most of the fields are unused at the moment struct OBSetEnableReferenced { asset_type: LayerEntryType, instance_id: u32, #[br(map = read_bool_from::)] ob_set_enable: bool, #[br(map = read_bool_from::)] ob_set_emissive_enable: bool, padding: [u8; 2], } #[binrw] #[derive(Debug)] #[brw(little)] #[allow(dead_code)] // most of the fields are unused at the moment struct LgbHeader { // Example: "LGB1" file_id: u32, // File size *including* this header file_size: i32, total_chunk_count: i32, } #[binrw] #[derive(Debug)] #[br(import(string_heap: &StringHeap), stream = r)] #[bw(import(string_heap: &mut StringHeap))] #[brw(little)] #[allow(dead_code)] // most of the fields are unused at the moment struct LayerChunkHeader { chunk_id: u32, chunk_size: i32, layer_group_id: i32, #[brw(args(string_heap))] pub name: HeapString, layer_offset: i32, layer_count: i32, } const LAYER_CHUNK_HEADER_SIZE: usize = 24; #[binread] #[derive(Debug)] #[br(little)] #[br(import(string_heap: &StringHeap))] #[allow(dead_code)] // most of the fields are unused at the moment pub struct InstanceObject { asset_type: LayerEntryType, pub instance_id: u32, #[br(args(string_heap))] pub name: HeapString, pub transform: Transformation, #[br(args(&asset_type, string_heap))] pub data: LayerEntryData, } #[derive(Debug)] pub struct Layer { pub header: LayerHeader, pub objects: Vec, } #[derive(Debug)] pub struct LayerChunk { // Example: "LGP1" pub chunk_id: u32, pub layer_group_id: i32, pub name: String, pub layers: Vec, } #[derive(Debug)] pub struct LayerGroup { pub file_id: u32, pub chunks: Vec, } impl LayerGroup { /// Reads an existing PBD file pub fn from_existing(buffer: ByteSpan) -> Option { let mut cursor = Cursor::new(buffer); let file_header = LgbHeader::read(&mut cursor).unwrap(); if file_header.file_size <= 0 || file_header.total_chunk_count <= 0 { return None; } // yes, for some reason it begins at 8 bytes in?!?! let chunk_string_heap = StringHeap::from(cursor.position() + 8); let chunk_header = LayerChunkHeader::read_le_args(&mut cursor, (&chunk_string_heap,)).unwrap(); if chunk_header.chunk_size <= 0 { return None; } let old_pos = cursor.position(); let mut layer_offsets = vec![0i32; chunk_header.layer_count as usize]; for i in 0..chunk_header.layer_count { layer_offsets[i as usize] = cursor.read_le::().unwrap(); } let mut layers = Vec::new(); for i in 0..chunk_header.layer_count { cursor .seek(SeekFrom::Start(old_pos + layer_offsets[i as usize] as u64)) .unwrap(); let old_pos = cursor.position(); let string_heap = StringHeap::from(old_pos); let data_heap = StringHeap::from(old_pos); let header = LayerHeader::read_le_args(&mut cursor, (&data_heap, &string_heap)).unwrap(); let mut objects = Vec::new(); // read instance objects { let mut instance_offsets = vec![0i32; header.instance_object_count as usize]; for i in 0..header.instance_object_count { instance_offsets[i as usize] = cursor.read_le::().unwrap(); } for i in 0..header.instance_object_count { cursor .seek(SeekFrom::Start( old_pos + header.instance_object_offset as u64 + instance_offsets[i as usize] as u64, )) .unwrap(); let start = cursor.stream_position().unwrap(); let string_heap = StringHeap::from(start); objects .push(InstanceObject::read_le_args(&mut cursor, (&string_heap,)).unwrap()); } } // read ob set referenced { cursor .seek(SeekFrom::Start( old_pos + header.ob_set_referenced_list as u64, )) .unwrap(); for _ in 0..header.ob_set_referenced_list_count { OBSetReferenced::read(&mut cursor).unwrap(); } } // read ob set enable referenced list { cursor .seek(SeekFrom::Start( old_pos + header.ob_set_enable_referenced_list as u64, )) .unwrap(); for _ in 0..header.ob_set_enable_referenced_list_count { OBSetEnableReferenced::read(&mut cursor).unwrap(); } } layers.push(Layer { header, objects }); } let layer_chunk = LayerChunk { chunk_id: chunk_header.chunk_id, layer_group_id: chunk_header.layer_group_id, name: chunk_header.name.value, layers, }; Some(LayerGroup { file_id: file_header.file_id, chunks: vec![layer_chunk], }) } pub fn write_to_buffer(&self) -> Option { let mut buffer = ByteBuffer::new(); { let mut cursor = Cursor::new(&mut buffer); // skip header, will be writing it later cursor .seek(SeekFrom::Start(std::mem::size_of::() as u64)) .unwrap(); // base offset for deferred data let mut data_base = cursor.stream_position().unwrap(); let mut chunk_data_heap = StringHeap { pos: data_base + 4, bytes: Vec::new(), free_pos: data_base + 4, }; let mut chunk_string_heap = StringHeap { pos: data_base + 4, bytes: Vec::new(), free_pos: data_base + 4, }; // we will write this later, when we have a working string heap let layer_chunk_header_pos = cursor.stream_position().unwrap(); cursor .seek(SeekFrom::Current(LAYER_CHUNK_HEADER_SIZE as i64)) .unwrap(); // skip offsets for now, they will be written later let offset_pos = cursor.position(); cursor .seek(SeekFrom::Current( (std::mem::size_of::() * self.chunks[0].layers.len()) as i64, )) .ok()?; let mut offsets: Vec = Vec::new(); let layer_data_offset = cursor.position(); // first pass: write layers, we want to get a correct *chunk_data_heap* for layer in &self.chunks[0].layers { // set offset // this is also used to reference positions inside this layer let layer_offset = cursor.position() as i32; offsets.push(layer_offset); layer .header .write_le_args(&mut cursor, (&mut chunk_data_heap, &mut chunk_string_heap)) .ok()?; } // make sure the heaps are at the end of the layer data data_base += cursor.stream_position().unwrap() - layer_data_offset; // second pass: write layers again, we want to get a correct *chunk_string_heap* now that we know of the size of chunk_data_heap chunk_string_heap = StringHeap { pos: data_base + 4 + chunk_data_heap.bytes.len() as u64, bytes: Vec::new(), free_pos: data_base + 4 + chunk_data_heap.bytes.len() as u64, }; chunk_data_heap = StringHeap { pos: data_base + 4, bytes: Vec::new(), free_pos: data_base + 4, }; // write header now, because it has a string cursor .seek(SeekFrom::Start(layer_chunk_header_pos)) .unwrap(); // TODO: support multiple layer chunks let layer_chunk = LayerChunkHeader { chunk_id: self.chunks[0].chunk_id, chunk_size: 24, // double lol layer_group_id: self.chunks[0].layer_group_id, name: HeapString { value: self.chunks[0].name.clone(), }, layer_offset: 16, // lol layer_count: self.chunks[0].layers.len() as i32, }; layer_chunk .write_le_args(&mut cursor, (&mut chunk_string_heap,)) .ok()?; // now write the layer data for the final time cursor.seek(SeekFrom::Start(layer_data_offset)).unwrap(); for layer in &self.chunks[0].layers { layer .header .write_le_args(&mut cursor, (&mut chunk_data_heap, &mut chunk_string_heap)) .ok()?; } // write the heaps chunk_data_heap.write_le(&mut cursor).ok()?; chunk_string_heap.write_le(&mut cursor).ok()?; // write offsets assert_eq!(offsets.len(), self.chunks[0].layers.len()); cursor.seek(SeekFrom::Start(offset_pos)).ok()?; for offset in offsets { offset.write_le(&mut cursor).ok()?; } } let file_size = buffer.len() as i32; { let mut cursor = Cursor::new(&mut buffer); // write the header, now that we now the file size cursor.seek(SeekFrom::Start(0)).ok()?; let lgb_header = LgbHeader { file_id: self.file_id, file_size, total_chunk_count: self.chunks.len() as i32, }; lgb_header.write_le(&mut cursor).ok()?; } Some(buffer) } } #[cfg(test)] mod tests { use std::fs::read; use std::path::PathBuf; use super::*; #[test] fn test_invalid() { let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); d.push("resources/tests"); d.push("random"); // Feeding it invalid data should not panic LayerGroup::from_existing(&read(d).unwrap()); } #[test] fn read_empty_planlive() { let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); d.push("resources/tests"); d.push("empty_planlive.lgb"); let lgb = LayerGroup::from_existing(&read(d).unwrap()).unwrap(); assert_eq!(lgb.file_id, LGB1_ID); assert_eq!(lgb.chunks.len(), 1); let chunk = &lgb.chunks[0]; assert_eq!(chunk.chunk_id, LGP1_ID); assert_eq!(chunk.layer_group_id, 261); assert_eq!(chunk.name, "PlanLive".to_string()); assert!(chunk.layers.is_empty()); } #[test] fn write_empty_planlive() { let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); d.push("resources/tests"); d.push("empty_planlive.lgb"); let good_lgb_bytes = read(d).unwrap(); let lgb = LayerGroup { file_id: LGB1_ID, chunks: vec![LayerChunk { chunk_id: LGP1_ID, layer_group_id: 261, name: "PlanLive".to_string(), layers: Vec::new(), }], }; assert_eq!(lgb.write_to_buffer().unwrap(), good_lgb_bytes); } }