physis/
exd.rs

1// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::io::{Cursor, Seek, SeekFrom};
5
6use binrw::binrw;
7use binrw::helpers::until_eof;
8use binrw::{BinRead, Endian};
9
10use crate::ByteSpan;
11use crate::common::Language;
12use crate::exh::{ColumnDataType, EXH, ExcelColumnDefinition, ExcelDataPagination};
13
14#[binrw]
15#[brw(magic = b"EXDF")]
16#[brw(big)]
17#[allow(dead_code)]
18struct EXDHeader {
19    version: u16,
20
21    #[br(pad_before = 2)]
22    #[br(pad_after = 20)]
23    index_size: u32,
24}
25
26#[binrw]
27#[brw(big)]
28struct ExcelDataOffset {
29    row_id: u32,
30    pub offset: u32,
31}
32
33#[binrw]
34#[brw(big)]
35#[allow(dead_code)]
36struct ExcelDataRowHeader {
37    data_size: u32,
38    row_count: u16,
39}
40
41#[binrw]
42#[brw(big)]
43#[allow(dead_code)]
44pub struct EXD {
45    header: EXDHeader,
46
47    #[br(count = header.index_size / core::mem::size_of::<ExcelDataOffset>() as u32)]
48    data_offsets: Vec<ExcelDataOffset>,
49
50    #[br(seek_before = SeekFrom::Start(0), parse_with = until_eof)]
51    data: Vec<u8>,
52}
53
54#[derive(Debug)]
55pub enum ColumnData {
56    String(String),
57    Bool(bool),
58    Int8(i8),
59    UInt8(u8),
60    Int16(i16),
61    UInt16(u16),
62    Int32(i32),
63    UInt32(u32),
64    Float32(f32),
65    Int64(i64),
66    UInt64(u64),
67}
68
69#[derive(Debug)]
70pub struct ExcelRow {
71    pub data: Vec<ColumnData>,
72}
73
74impl EXD {
75    pub fn from_existing(buffer: ByteSpan) -> Option<EXD> {
76        EXD::read(&mut Cursor::new(&buffer)).ok()
77    }
78
79    pub fn read_row(&self, exh: &EXH, id: u32) -> Option<Vec<ExcelRow>> {
80        let mut cursor = Cursor::new(&self.data);
81
82        for offset in &self.data_offsets {
83            if offset.row_id == id {
84                cursor.seek(SeekFrom::Start(offset.offset.into())).ok()?;
85
86                let row_header = ExcelDataRowHeader::read(&mut cursor).ok()?;
87
88                let header_offset = offset.offset + 6; // std::mem::size_of::<ExcelDataRowHeader>() as u32;
89
90                let mut read_row = |row_offset: u32| -> Option<ExcelRow> {
91                    let mut subrow = ExcelRow {
92                        data: Vec::with_capacity(exh.column_definitions.len()),
93                    };
94
95                    for column in &exh.column_definitions {
96                        cursor
97                            .seek(SeekFrom::Start((row_offset + column.offset as u32).into()))
98                            .ok()?;
99
100                        subrow
101                            .data
102                            .push(Self::read_column(&mut cursor, exh, row_offset, column).unwrap());
103                    }
104
105                    Some(subrow)
106                };
107
108                return if row_header.row_count > 1 {
109                    let mut rows = Vec::new();
110                    for i in 0..row_header.row_count {
111                        let subrow_offset =
112                            header_offset + (i * exh.header.data_offset + 2 * (i + 1)) as u32;
113
114                        rows.push(read_row(subrow_offset).unwrap());
115                    }
116                    Some(rows)
117                } else {
118                    Some(vec![read_row(header_offset).unwrap()])
119                };
120            }
121        }
122
123        None
124    }
125
126    fn read_data_raw<Z: BinRead<Args<'static> = ()>>(cursor: &mut Cursor<&Vec<u8>>) -> Option<Z> {
127        Z::read_options(cursor, Endian::Big, ()).ok()
128    }
129
130    fn read_column(
131        cursor: &mut Cursor<&Vec<u8>>,
132        exh: &EXH,
133        row_offset: u32,
134        column: &ExcelColumnDefinition,
135    ) -> Option<ColumnData> {
136        let mut read_packed_bool = |shift: i32| -> bool {
137            let bit = 1 << shift;
138            let bool_data: i32 = Self::read_data_raw(cursor).unwrap_or(0);
139
140            (bool_data & bit) == bit
141        };
142
143        match column.data_type {
144            ColumnDataType::String => {
145                let string_offset: u32 = Self::read_data_raw(cursor).unwrap();
146
147                cursor
148                    .seek(SeekFrom::Start(
149                        (row_offset + exh.header.data_offset as u32 + string_offset).into(),
150                    ))
151                    .ok()?;
152
153                let mut string = String::new();
154
155                let mut byte: u8 = Self::read_data_raw(cursor).unwrap();
156                while byte != 0 {
157                    string.push(byte as char);
158                    byte = Self::read_data_raw(cursor).unwrap();
159                }
160
161                Some(ColumnData::String(string))
162            }
163            ColumnDataType::Bool => {
164                // FIXME: i believe Bool is int8?
165                let bool_data: i32 = Self::read_data_raw(cursor).unwrap();
166
167                Some(ColumnData::Bool(bool_data == 1))
168            }
169            ColumnDataType::Int8 => Some(ColumnData::Int8(Self::read_data_raw(cursor).unwrap())),
170            ColumnDataType::UInt8 => Some(ColumnData::UInt8(Self::read_data_raw(cursor).unwrap())),
171            ColumnDataType::Int16 => Some(ColumnData::Int16(Self::read_data_raw(cursor).unwrap())),
172            ColumnDataType::UInt16 => {
173                Some(ColumnData::UInt16(Self::read_data_raw(cursor).unwrap()))
174            }
175            ColumnDataType::Int32 => Some(ColumnData::Int32(Self::read_data_raw(cursor).unwrap())),
176            ColumnDataType::UInt32 => {
177                Some(ColumnData::UInt32(Self::read_data_raw(cursor).unwrap()))
178            }
179            ColumnDataType::Float32 => {
180                Some(ColumnData::Float32(Self::read_data_raw(cursor).unwrap()))
181            }
182            ColumnDataType::Int64 => Some(ColumnData::Int64(Self::read_data_raw(cursor).unwrap())),
183            ColumnDataType::UInt64 => {
184                Some(ColumnData::UInt64(Self::read_data_raw(cursor).unwrap()))
185            }
186            ColumnDataType::PackedBool0 => Some(ColumnData::Bool(read_packed_bool(0))),
187            ColumnDataType::PackedBool1 => Some(ColumnData::Bool(read_packed_bool(1))),
188            ColumnDataType::PackedBool2 => Some(ColumnData::Bool(read_packed_bool(2))),
189            ColumnDataType::PackedBool3 => Some(ColumnData::Bool(read_packed_bool(3))),
190            ColumnDataType::PackedBool4 => Some(ColumnData::Bool(read_packed_bool(4))),
191            ColumnDataType::PackedBool5 => Some(ColumnData::Bool(read_packed_bool(5))),
192            ColumnDataType::PackedBool6 => Some(ColumnData::Bool(read_packed_bool(6))),
193            ColumnDataType::PackedBool7 => Some(ColumnData::Bool(read_packed_bool(7))),
194        }
195    }
196
197    pub fn calculate_filename(
198        name: &str,
199        language: Language,
200        page: &ExcelDataPagination,
201    ) -> String {
202        use crate::common::get_language_code;
203
204        match language {
205            Language::None => {
206                format!("{name}_{}.exd", page.start_id)
207            }
208            lang => {
209                format!("{name}_{}_{}.exd", page.start_id, get_language_code(&lang))
210            }
211        }
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use crate::exh::EXHHeader;
218    use std::fs::read;
219    use std::path::PathBuf;
220
221    use super::*;
222
223    #[test]
224    fn test_invalid() {
225        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
226        d.push("resources/tests");
227        d.push("random");
228
229        let exh = EXH {
230            header: EXHHeader {
231                version: 0,
232                data_offset: 0,
233                column_count: 0,
234                page_count: 0,
235                language_count: 0,
236                row_count: 0,
237            },
238            column_definitions: vec![],
239            pages: vec![],
240            languages: vec![],
241        };
242
243        // Feeding it invalid data should not panic
244        EXD::from_existing(&read(d).unwrap());
245    }
246}