From 200cf73248526a01b325f06781f1aca10e831105 Mon Sep 17 00:00:00 2001 From: Tyler Hallada Date: Thu, 20 May 2021 01:33:11 -0400 Subject: [PATCH] Add parser and cli, basic extraction done --- .gitignore | 1 + Cargo.lock | 291 ++++++++++++++++++++++++++++++++++ Cargo.toml | 24 +++ src/bin/cli.rs | 73 +++++++++ src/lib.rs | 6 + src/parser.rs | 421 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 816 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/bin/cli.rs create mode 100644 src/lib.rs create mode 100644 src/parser.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7cdb018 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,291 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anyhow" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" + +[[package]] +name = "argh" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91792f088f87cdc7a2cfb1d617fa5ea18d7f1dc22ef0e1b5f82f3157cdc522be" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4eb0c0c120ad477412dc95a4ce31e38f2113e46bd13511253f79196ca68b067" +dependencies = [ + "argh_shared", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "781f336cc9826dbaddb9754cb5db61e64cab4f69668bd19dcc4a0394a86f4cb1" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bitvec" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "flate2" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "lexical-core" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265d751d31d6780a3f956bb5b8022feba2d94eeee5a84ba64f4212eedca42213" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "skyrim_cell_dump" +version = "0.1.0" +dependencies = [ + "anyhow", + "argh", + "bitflags", + "flate2", + "nom", + "serde", + "serde_json", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..543c7e5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "skyrim_cell_dump" +version = "0.1.0" +authors = ["Tyler Hallada "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +argh = { version = "0.1", optional = true } +bitflags = "1.2" +flate2 = "1.0" +nom = "6" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", optional = true } + +[features] +build-binary = ["argh", "serde_json"] + +[[bin]] +name = "skyrim-cell-dump" +path = "src/bin/cli.rs" +required-features = ["build-binary"] \ No newline at end of file diff --git a/src/bin/cli.rs b/src/bin/cli.rs new file mode 100644 index 0000000..0ca6baf --- /dev/null +++ b/src/bin/cli.rs @@ -0,0 +1,73 @@ +use std::path::PathBuf; +use std::{fs::read, str::FromStr}; + +use anyhow::{anyhow, Error, Result}; +#[cfg(feature = "build-binary")] +use argh::FromArgs; + +use skyrim_cell_dump::parse_plugin; + +enum Format { + Json, + PlainText, +} + +impl FromStr for Format { + type Err = Error; + fn from_str(s: &str) -> Result { + match s.trim().to_lowercase().as_str() { + "json" => Ok(Format::Json), + "text" => Ok(Format::PlainText), + "plain" => Ok(Format::PlainText), + "plain_text" => Ok(Format::PlainText), + "plaintext" => Ok(Format::PlainText), + _ => Err(anyhow!("Unrecognized format {}", s)), + } + } +} + +#[derive(FromArgs)] +/// Extracts cell edits from a TES5 Skyrim plugin file +struct Args { + /// path to the plugin to parse + #[argh(positional)] + plugin: PathBuf, + /// format of the output (json or text) + #[argh(option, short = 'f', default = "Format::PlainText")] + format: Format, + /// pretty print json output + #[argh(switch, short = 'p')] + pretty: bool, +} + +fn main() { + let args: Args = argh::from_env(); + let plugin_contents = match read(&args.plugin) { + Ok(contents) => contents, + Err(error) => { + return eprintln!( + "Failed to read from plugin file {}: {}", + &args.plugin.to_string_lossy(), + error + ) + } + }; + let plugin = match parse_plugin(&plugin_contents) { + Ok(plugin) => plugin, + Err(error) => { + return eprintln!( + "Failed to parse plugin file {}: {}", + &args.plugin.to_string_lossy(), + error + ) + } + }; + + match args.format { + Format::PlainText => println!("{:#?}", &plugin), + Format::Json if args.pretty => { + println!("{}", serde_json::to_string_pretty(&plugin).unwrap()) + } + Format::Json => println!("{}", serde_json::to_string(&plugin).unwrap()), + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..77c7e19 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +#[macro_use] +extern crate bitflags; + +mod parser; + +pub use parser::{decompress_cells, parse_cell, parse_plugin}; diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..ac26f47 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,421 @@ +use std::io::Read; +use std::{convert::TryInto, str}; + +use anyhow::{anyhow, Result}; +use flate2::read::ZlibDecoder; +use nom::{ + branch::alt, + bytes::complete::{take, take_while}, + combinator::{map, map_res, verify}, + number::complete::{le_f32, le_i32, le_u16, le_u32}, + IResult, +}; +use serde::Serialize; + +const HEADER_SIZE: u32 = 24; + +#[derive(Debug, PartialEq, Serialize)] +pub struct Plugin<'a> { + pub header: PluginHeader<'a>, + pub cells: Vec, +} + +#[derive(Debug, PartialEq, Serialize)] +pub struct PluginHeader<'a> { + pub version: f32, + pub num_records_and_groups: i32, + pub next_object_id: u32, + pub author: Option<&'a str>, + pub description: Option<&'a str>, + pub masters: Vec<&'a str>, +} + +#[derive(Debug, PartialEq, Serialize)] +pub struct Cell { + pub form_id: u32, + pub editor_id: Option, + pub x: Option, + pub y: Option, + pub is_persistent: bool, +} + +#[derive(Debug)] +struct CellData { + editor_id: Option, + x: Option, + y: Option, +} + +#[derive(Debug)] +pub struct UnparsedCell<'a> { + form_id: u32, + is_compressed: bool, + is_persistent: bool, + data: &'a [u8], +} + +#[derive(Debug)] +pub struct DecompressedCell { + pub form_id: u32, + pub is_persistent: bool, + pub data: Vec, +} + +#[derive(Debug)] +struct GroupHeader<'a> { + size: u32, + label: &'a [u8; 4], + group_type: i32, + timestamp: u16, + version_control_info: u16, +} + +#[derive(Debug)] +struct RecordHeader<'a> { + record_type: &'a str, + size: u32, + flags: RecordFlags, + id: u32, + timestamp: u16, + version_control_info: u16, + version: u16, +} + +bitflags! { + struct RecordFlags: u32 { + const MASTER_FILE = 0x00000001; + const DELETED_GROUP = 0x00000010; + const DELETED_RECORD = 0x00000020; + const CONSTANT = 0x00000040; + const LOCALIZED = 0x00000080; + const INACCESSIBLE = 0x00000100; + const LIGHT_MASTER_FILE = 0x00000200; + const PERSISTENT_REFR = 0x00000400; + const INITIALLY_DISABLED = 0x00000800; + const IGNORED = 0x00001000; + const VISIBLE_WHEN_DISTANT = 0x00008000; + const RANDOM_ANIM_START = 0x00010000; + const OFF_LIMITS = 0x00020000; + const COMPRESSED = 0x00040000; + const CANT_WAIT = 0x00080000; + const IGNORE_OBJECT_INTERACTION = 0x00100000; + const IS_MARKER = 0x00800000; + const NO_AI_ACQUIRE = 0x02000000; + const NAVMESH_FILTER = 0x04000000; + const NAVMESH_BOUNDING_BOX = 0x08000000; + const REFLECTED_BY_AUTO_WATER = 0x10000000; + const DONT_HAVOK_SETTLE = 0x20000000; + const NO_RESPAWN = 0x40000000; + const MULTI_BOUND = 0x80000000; + } +} + +#[derive(Debug)] +enum Header<'a> { + Group(GroupHeader<'a>), + Record(RecordHeader<'a>), +} + +#[derive(Debug)] +struct FieldHeader<'a> { + field_type: &'a str, + size: u16, +} + +pub fn parse_cell<'a>( + input: &'a [u8], + form_id: u32, + is_persistent: bool, +) -> IResult<&'a [u8], Cell> { + let (input, cell_data) = parse_cell_fields(input)?; + Ok(( + input, + Cell { + form_id, + editor_id: cell_data.editor_id, + x: cell_data.x, + y: cell_data.y, + is_persistent, + }, + )) +} + +pub fn decompress_cells(unparsed_cells: Vec) -> Result> { + let mut decompressed_cells = Vec::new(); + for unparsed_cell in unparsed_cells { + let decompressed_data = if unparsed_cell.is_compressed { + let mut buf = Vec::new(); + let mut decoder = ZlibDecoder::new(&unparsed_cell.data[4..]); + decoder.read_to_end(&mut buf)?; + buf + } else { + unparsed_cell.data.to_vec() + }; + decompressed_cells.push(DecompressedCell { + form_id: unparsed_cell.form_id, + is_persistent: unparsed_cell.is_persistent, + data: decompressed_data, + }); + } + Ok(decompressed_cells) +} + +pub fn parse_header_and_cell_bytes( + input: &[u8], +) -> IResult<&[u8], (PluginHeader, Vec)> { + let (input, header) = parse_plugin_header(input)?; + let (input, unparsed_cells) = parse_group_data(input, input.len() as u32, 0)?; + Ok((input, (header, unparsed_cells))) +} + +pub fn parse_plugin(input: &[u8]) -> Result { + let (_, (header, unparsed_cells)) = parse_header_and_cell_bytes(&input) + .map_err(|_err| anyhow!("Failed to parse plugin header and find CELL data"))?; + let decompressed_cells = decompress_cells(unparsed_cells)?; + + let mut cells = Vec::new(); + for decompressed_cell in decompressed_cells { + let (_, cell) = parse_cell( + &decompressed_cell.data, + decompressed_cell.form_id, + decompressed_cell.is_persistent, + ) + .unwrap(); + cells.push(cell); + } + + Ok(Plugin { header, cells }) +} + +fn parse_group_data<'a>( + input: &'a [u8], + remaining_bytes: u32, + depth: usize, +) -> IResult<&'a [u8], Vec> { + let mut input = input; + let mut cells = vec![]; + let mut consumed_bytes = 0; + while !input.is_empty() && consumed_bytes < remaining_bytes { + let (remaining, record_header) = parse_header(input)?; + match record_header { + Header::Group(group_header) => { + if group_header.group_type == 0 { + // TODO: get rid of unwrap + let label = str::from_utf8(group_header.label).unwrap(); + if label != "WRLD" && label != "CELL" { + let (remaining, _) = take(group_header.size - HEADER_SIZE)(remaining)?; + input = remaining; + consumed_bytes += group_header.size; + continue; + } + } else if group_header.group_type == 7 { + // TODO: DRY + let (remaining, _) = take(group_header.size - HEADER_SIZE)(remaining)?; + input = remaining; + consumed_bytes += group_header.size; + continue; + } + let (remaining, mut inner_cells) = + parse_group_data(remaining, group_header.size - HEADER_SIZE, depth + 1)?; + cells.append(&mut inner_cells); + input = remaining; + consumed_bytes += group_header.size; + } + Header::Record(record_header) => match record_header.record_type { + "CELL" => { + let (remaining, data) = take(record_header.size)(remaining)?; + cells.push(UnparsedCell { + form_id: record_header.id, + is_compressed: record_header.flags.contains(RecordFlags::COMPRESSED), + is_persistent: record_header.flags.contains(RecordFlags::PERSISTENT_REFR), + data, + }); + input = remaining; + consumed_bytes += record_header.size + HEADER_SIZE; + } + _ => { + let (remaining, _) = take(record_header.size)(remaining)?; + input = remaining; + consumed_bytes += record_header.size + HEADER_SIZE; + } + }, + } + } + Ok((input, cells)) +} + +fn parse_plugin_header(input: &[u8]) -> IResult<&[u8], PluginHeader> { + let (mut input, _tes4) = verify(parse_record_header, |record_header| { + record_header.record_type == "TES4" + })(input)?; + let (remaining, _hedr) = verify(parse_field_header, |field_header| { + field_header.field_type == "HEDR" + })(input)?; + input = remaining; + let (remaining, (version, num_records_and_groups, next_object_id)) = parse_hedr_fields(input)?; + input = remaining; + let mut author = None; + let mut description = None; + let mut masters = vec![]; + loop { + let (remaining, field) = parse_field_header(input)?; + input = remaining; + match field.field_type { + "CNAM" => { + let (remaining, author_str) = parse_zstring(input)?; + input = remaining; + author = Some(author_str); + } + "SNAM" => { + let (remaining, desc_str) = parse_zstring(input)?; + input = remaining; + description = Some(desc_str); + } + "MAST" => { + let (remaining, master_str) = parse_zstring(input)?; + input = remaining; + masters.push(master_str); + } + "INTV" => { + let (remaining, _) = take(field.size)(input)?; + input = remaining; + break; + } + _ => { + let (remaining, _) = take(field.size)(input)?; + input = remaining; + } + } + } + Ok(( + input, + PluginHeader { + version, + num_records_and_groups, + next_object_id, + author, + description, + masters, + }, + )) +} + +fn parse_group_header(input: &[u8]) -> IResult<&[u8], GroupHeader> { + let (input, _record_type) = + verify(parse_4char, |record_type: &str| record_type == "GRUP")(input)?; + let (input, size) = le_u32(input)?; + let (input, label) = map_res(take(4usize), |bytes: &[u8]| bytes.try_into())(input)?; + let (input, group_type) = le_i32(input)?; + let (input, timestamp) = le_u16(input)?; + let (input, version_control_info) = le_u16(input)?; + let (input, _) = take(4usize)(input)?; + Ok(( + input, + GroupHeader { + size, + label, + group_type, + timestamp, + version_control_info, + }, + )) +} + +fn parse_record_header(input: &[u8]) -> IResult<&[u8], RecordHeader> { + let (input, record_type) = + verify(parse_4char, |record_type: &str| record_type != "GRUP")(input)?; + let (input, size) = le_u32(input)?; + let (input, flags) = map_res(le_u32, |bits| { + RecordFlags::from_bits(bits).ok_or("bad record flag") + })(input)?; + let (input, id) = le_u32(input)?; + let (input, timestamp) = le_u16(input)?; + let (input, version_control_info) = le_u16(input)?; + let (input, version) = le_u16(input)?; + let (input, _) = take(2usize)(input)?; + Ok(( + input, + RecordHeader { + record_type, + size, + flags, + id, + timestamp, + version_control_info, + version, + }, + )) +} + +fn parse_header(input: &[u8]) -> IResult<&[u8], Header> { + alt(( + map(parse_group_header, |group_header| { + Header::Group(group_header) + }), + map(parse_record_header, |record_header| { + Header::Record(record_header) + }), + ))(input) +} + +fn parse_field_header(input: &[u8]) -> IResult<&[u8], FieldHeader> { + let (input, field_type) = parse_4char(input)?; + if field_type == "XXXX" { + todo!() + } + let (input, size) = le_u16(input)?; + // let (input, data) = take(size)(input)?; + Ok((input, FieldHeader { field_type, size })) +} + +fn parse_hedr_fields(input: &[u8]) -> IResult<&[u8], (f32, i32, u32)> { + let (input, version) = le_f32(input)?; + let (input, num_records_and_groups) = le_i32(input)?; + let (input, next_object_id) = le_u32(input)?; + Ok((input, (version, num_records_and_groups, next_object_id))) +} + +fn parse_cell_fields<'a>(input: &'a [u8]) -> IResult<&'a [u8], CellData> { + let mut cell_data = CellData { + editor_id: None, + x: None, + y: None, + }; + let mut input = input; + while !input.is_empty() { + let (remaining, field) = parse_field_header(input)?; + input = remaining; + match field.field_type { + "EDID" => { + let (remaining, editor_id) = parse_zstring(input)?; + cell_data.editor_id = Some(editor_id.to_string()); + input = remaining; + } + "XCLC" => { + let (remaining, x) = le_i32(input)?; + let (remaining, y) = le_i32(remaining)?; + cell_data.x = Some(x); + cell_data.y = Some(y); + let (remaining, _) = take(4usize)(remaining)?; + input = remaining; + } + _ => { + let (remaining, _) = take(field.size)(input)?; + input = remaining; + } + } + } + Ok((input, cell_data)) +} + +fn parse_4char(input: &[u8]) -> IResult<&[u8], &str> { + map_res(take(4usize), |bytes: &[u8]| str::from_utf8(bytes))(input) +} + +fn parse_zstring(input: &[u8]) -> IResult<&[u8], &str> { + let (input, zstring) = map_res(take_while(|byte| byte != 0), |bytes: &[u8]| { + str::from_utf8(bytes) + })(input)?; + let (input, _) = take(1usize)(input)?; + Ok((input, zstring)) +}