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