physis/
exh.rs

1// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4#![allow(clippy::unnecessary_fallible_conversions)] // This wrongly trips on binrw code
5#![allow(unused_variables)] // just binrw things with br(temp)
6
7use std::io::BufWriter;
8use std::io::Cursor;
9
10use binrw::BinRead;
11use binrw::BinWrite;
12use binrw::binrw;
13
14use crate::ByteBuffer;
15use crate::ByteSpan;
16use crate::common::Language;
17
18#[binrw]
19#[brw(magic = b"EXHF")]
20#[brw(big)]
21#[derive(Debug)]
22pub struct EXHHeader {
23    pub(crate) version: u16,
24
25    pub data_offset: u16, // TODO: might not be an offset
26    pub(crate) column_count: u16,
27    pub(crate) page_count: u16,
28    pub(crate) language_count: u16,
29
30    /// Usually 0
31    pub unk1: u16,
32
33    #[br(temp)]
34    #[bw(calc = 0x010000)] // always this value??
35    pub unk2: u32,
36
37    #[brw(pad_after = 8)] // padding
38    pub row_count: u32,
39}
40
41#[binrw]
42#[brw(repr(u16))]
43#[repr(u16)]
44#[derive(PartialEq, Eq, Clone, Copy, Debug)]
45pub enum ColumnDataType {
46    String = 0x0,
47    Bool = 0x1,
48    Int8 = 0x2,
49    UInt8 = 0x3,
50    Int16 = 0x4,
51    UInt16 = 0x5,
52    Int32 = 0x6,
53    UInt32 = 0x7,
54    Float32 = 0x9,
55    Int64 = 0xA,
56    UInt64 = 0xB,
57
58    PackedBool0 = 0x19,
59    PackedBool1 = 0x1A,
60    PackedBool2 = 0x1B,
61    PackedBool3 = 0x1C,
62    PackedBool4 = 0x1D,
63    PackedBool5 = 0x1E,
64    PackedBool6 = 0x1F,
65    PackedBool7 = 0x20,
66}
67
68#[binrw]
69#[brw(big)]
70#[derive(Debug, Copy, Clone)]
71pub struct ExcelColumnDefinition {
72    pub data_type: ColumnDataType,
73    pub offset: u16,
74}
75
76#[binrw]
77#[brw(big)]
78#[allow(dead_code)]
79#[derive(Debug)]
80pub struct ExcelDataPagination {
81    pub start_id: u32,
82    pub row_count: u32,
83}
84
85#[binrw]
86#[brw(big)]
87#[allow(dead_code)]
88#[derive(Debug)]
89pub struct EXH {
90    pub header: EXHHeader,
91
92    #[br(count = header.column_count)]
93    pub column_definitions: Vec<ExcelColumnDefinition>,
94
95    #[br(count = header.page_count)]
96    pub pages: Vec<ExcelDataPagination>,
97
98    #[br(count = header.language_count)]
99    #[brw(pad_after = 1)] // \0
100    pub languages: Vec<Language>,
101}
102
103impl EXH {
104    pub fn from_existing(buffer: ByteSpan) -> Option<EXH> {
105        EXH::read(&mut Cursor::new(&buffer)).ok()
106    }
107
108    pub fn write_to_buffer(&self) -> Option<ByteBuffer> {
109        let mut buffer = ByteBuffer::new();
110
111        {
112            let cursor = Cursor::new(&mut buffer);
113            let mut writer = BufWriter::new(cursor);
114
115            self.write_args(&mut writer, ()).unwrap();
116        }
117
118        Some(buffer)
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use std::fs::read;
125    use std::path::PathBuf;
126
127    use super::*;
128
129    #[test]
130    fn test_invalid() {
131        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
132        d.push("resources/tests");
133        d.push("random");
134
135        // Feeding it invalid data should not panic
136        EXH::from_existing(&read(d).unwrap());
137    }
138
139    // simple EXH to read, just one page
140    #[test]
141    fn test_read() {
142        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
143        d.push("resources/tests");
144        d.push("gcshop.exh");
145
146        let exh = EXH::from_existing(&read(d).unwrap()).unwrap();
147
148        // header
149        assert_eq!(exh.header.version, 3);
150        assert_eq!(exh.header.data_offset, 4);
151        assert_eq!(exh.header.column_count, 1);
152        assert_eq!(exh.header.page_count, 1);
153        assert_eq!(exh.header.language_count, 1);
154        assert_eq!(exh.header.row_count, 4);
155
156        // column definitions
157        assert_eq!(exh.column_definitions.len(), 1);
158        assert_eq!(exh.column_definitions[0].data_type, ColumnDataType::Int8);
159        assert_eq!(exh.column_definitions[0].offset, 0);
160
161        // pages
162        assert_eq!(exh.pages.len(), 1);
163        assert_eq!(exh.pages[0].start_id, 1441792);
164        assert_eq!(exh.pages[0].row_count, 4);
165
166        // languages
167        assert_eq!(exh.languages.len(), 1);
168        assert_eq!(exh.languages[0], Language::None);
169    }
170
171    // simple EXH to write, only one page
172    #[test]
173    fn test_write() {
174        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
175        d.push("resources/tests");
176        d.push("gcshop.exh");
177
178        let expected_exh_bytes = read(d).unwrap();
179        let expected_exh = EXH::from_existing(&expected_exh_bytes).unwrap();
180
181        let actual_exh_bytes = expected_exh.write_to_buffer().unwrap();
182
183        assert_eq!(actual_exh_bytes, expected_exh_bytes);
184    }
185}