Skip to main content

mesh_data_tile/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::fmt;
4
5mod common;
6mod consts;
7mod decoder;
8mod encoder;
9
10pub use consts::{TILE_FIXED_HEADER_LENGTH, TILE_VERSION_MAJOR};
11pub use decoder::{decode_payload_values, decode_tile_minimal, inspect_tile};
12pub use encoder::{encode_payload_values, encode_tile};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum MeshKind {
16    JisX0410,
17    Xyz,
18}
19
20impl MeshKind {
21    pub(crate) fn code(self) -> u8 {
22        match self {
23            Self::JisX0410 => 1,
24            Self::Xyz => 2,
25        }
26    }
27
28    pub(crate) fn from_code(code: u8) -> Result<Self> {
29        match code {
30            1 => Ok(Self::JisX0410),
31            2 => Ok(Self::Xyz),
32            _ => Err(TileError::new(
33                TileErrorCode::InvalidFieldValue,
34                format!("Invalid mesh_kind code {code}."),
35            )),
36        }
37    }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum Endianness {
42    Little,
43    Big,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum DType {
48    Uint8,
49    Int8,
50    Uint16,
51    Int16,
52    Uint32,
53    Int32,
54    Float32,
55    Float64,
56}
57
58impl DType {
59    pub(crate) fn code(self) -> u8 {
60        match self {
61            Self::Uint8 => 0,
62            Self::Int8 => 1,
63            Self::Uint16 => 2,
64            Self::Int16 => 3,
65            Self::Uint32 => 4,
66            Self::Int32 => 5,
67            Self::Float32 => 6,
68            Self::Float64 => 7,
69        }
70    }
71
72    pub(crate) fn from_code(code: u8) -> Result<Self> {
73        match code {
74            0 => Ok(Self::Uint8),
75            1 => Ok(Self::Int8),
76            2 => Ok(Self::Uint16),
77            3 => Ok(Self::Int16),
78            4 => Ok(Self::Uint32),
79            5 => Ok(Self::Int32),
80            6 => Ok(Self::Float32),
81            7 => Ok(Self::Float64),
82            _ => Err(TileError::new(
83                TileErrorCode::InvalidFieldValue,
84                format!("Unsupported packed dtype code {code}."),
85            )),
86        }
87    }
88
89    pub fn byte_size(self) -> usize {
90        match self {
91            Self::Uint8 | Self::Int8 => 1,
92            Self::Uint16 | Self::Int16 => 2,
93            Self::Uint32 | Self::Int32 | Self::Float32 => 4,
94            Self::Float64 => 8,
95        }
96    }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum CompressionMode {
101    None,
102    DeflateRaw,
103}
104
105impl Default for CompressionMode {
106    fn default() -> Self {
107        Self::None
108    }
109}
110
111impl CompressionMode {
112    pub(crate) fn code(self) -> u8 {
113        match self {
114            Self::None => 0,
115            Self::DeflateRaw => 1,
116        }
117    }
118
119    pub(crate) fn from_code(code: u8) -> Result<Self> {
120        match code {
121            0 => Ok(Self::None),
122            1 => Ok(Self::DeflateRaw),
123            _ => Err(TileError::new(
124                TileErrorCode::InvalidFieldValue,
125                format!("Invalid compression code {code}."),
126            )),
127        }
128    }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct TileDimensions {
133    pub rows: u32,
134    pub cols: u32,
135    pub bands: u8,
136}
137
138impl TileDimensions {
139    pub(crate) fn validate(self) -> Result<()> {
140        if self.rows == 0 || self.cols == 0 || self.bands == 0 {
141            return Err(TileError::new(
142                TileErrorCode::InvalidFieldValue,
143                "rows, cols, and bands must be > 0.",
144            ));
145        }
146        Ok(())
147    }
148
149    pub(crate) fn total_samples(self) -> Result<u64> {
150        let rows = u64::from(self.rows);
151        let cols = u64::from(self.cols);
152        let bands = u64::from(self.bands);
153        rows.checked_mul(cols)
154            .and_then(|v| v.checked_mul(bands))
155            .ok_or_else(|| {
156                TileError::new(
157                    TileErrorCode::InvalidFieldValue,
158                    "Invalid dimensions resulting in overflowed sample count.",
159                )
160            })
161    }
162}
163
164#[derive(Debug, Clone, PartialEq)]
165pub struct TileHeader {
166    pub format_major: u8,
167    pub tile_id: u64,
168    pub mesh_kind: MeshKind,
169    pub dtype: DType,
170    pub endianness: Endianness,
171    pub compression: CompressionMode,
172    pub dimensions: TileDimensions,
173    pub no_data_kind: u8,
174    pub no_data_value_raw: [u8; 8],
175    pub no_data: Option<f64>,
176    pub payload_uncompressed_bytes: u64,
177    pub payload_compressed_bytes: u64,
178    pub payload_crc32: u32,
179    pub header_crc32: u32,
180}
181
182#[derive(Debug, Clone)]
183pub struct TileEncodeInput<'a> {
184    pub tile_id: u64,
185    pub mesh_kind: MeshKind,
186    pub dtype: DType,
187    pub endianness: Endianness,
188    pub compression: CompressionMode,
189    pub dimensions: TileDimensions,
190    pub no_data: Option<f64>,
191    pub payload: &'a [u8],
192}
193
194#[derive(Debug, Clone, PartialEq)]
195pub struct EncodedTile {
196    pub bytes: Vec<u8>,
197    pub header: TileHeader,
198}
199
200#[derive(Debug, Clone, PartialEq)]
201pub struct DecodedTile {
202    pub header: TileHeader,
203    pub payload: Vec<u8>,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum TileErrorCode {
208    InvalidMagic,
209    UnsupportedVersion,
210    InvalidHeaderLength,
211    InvalidFieldValue,
212    MissingRequiredField,
213    HeaderChecksumMismatch,
214    InvalidPayloadLength,
215    UnsupportedCompression,
216    CompressionFailed,
217    DecompressionFailed,
218    PayloadChecksumMismatch,
219}
220
221impl TileErrorCode {
222    pub fn as_str(self) -> &'static str {
223        match self {
224            Self::InvalidMagic => "INVALID_MAGIC",
225            Self::UnsupportedVersion => "UNSUPPORTED_VERSION",
226            Self::InvalidHeaderLength => "INVALID_HEADER_LENGTH",
227            Self::InvalidFieldValue => "INVALID_FIELD_VALUE",
228            Self::MissingRequiredField => "MISSING_REQUIRED_FIELD",
229            Self::HeaderChecksumMismatch => "HEADER_CHECKSUM_MISMATCH",
230            Self::InvalidPayloadLength => "INVALID_PAYLOAD_LENGTH",
231            Self::UnsupportedCompression => "UNSUPPORTED_COMPRESSION",
232            Self::CompressionFailed => "COMPRESSION_FAILED",
233            Self::DecompressionFailed => "DECOMPRESSION_FAILED",
234            Self::PayloadChecksumMismatch => "PAYLOAD_CHECKSUM_MISMATCH",
235        }
236    }
237}
238
239impl fmt::Display for TileErrorCode {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        write!(f, "{}", self.as_str())
242    }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct TileError {
247    pub code: TileErrorCode,
248    pub message: String,
249}
250
251impl TileError {
252    pub fn new(code: TileErrorCode, message: impl Into<String>) -> Self {
253        Self {
254            code,
255            message: message.into(),
256        }
257    }
258}
259
260impl fmt::Display for TileError {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        write!(f, "{}: {}", self.code, self.message)
263    }
264}
265
266impl std::error::Error for TileError {}
267
268pub type Result<T> = std::result::Result<T, TileError>;
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    fn tile_dims() -> TileDimensions {
275        TileDimensions {
276            rows: 2,
277            cols: 2,
278            bands: 1,
279        }
280    }
281
282    #[test]
283    fn roundtrip_uncompressed_payload() {
284        let payload =
285            encode_payload_values(DType::Uint16, Endianness::Little, &[10.0, 20.0, 30.0, 40.0])
286                .expect("encode payload values");
287
288        let encoded = encode_tile(TileEncodeInput {
289            tile_id: 42,
290            mesh_kind: MeshKind::JisX0410,
291            dtype: DType::Uint16,
292            endianness: Endianness::Little,
293            compression: CompressionMode::None,
294            dimensions: tile_dims(),
295            no_data: None,
296            payload: &payload,
297        })
298        .expect("encode tile");
299
300        let decoded = decode_tile_minimal(&encoded.bytes).expect("decode tile");
301        assert_eq!(decoded.header.tile_id, 42);
302        assert_eq!(decoded.header.compression, CompressionMode::None);
303        assert_eq!(decoded.payload, payload);
304
305        let values = decode_payload_values(
306            decoded.header.dtype,
307            decoded.header.endianness,
308            &decoded.payload,
309        )
310        .expect("decode payload values");
311        assert_eq!(values, vec![10.0, 20.0, 30.0, 40.0]);
312    }
313
314    #[test]
315    fn roundtrip_deflate_payload() {
316        let payload =
317            encode_payload_values(DType::Uint16, Endianness::Little, &[1.0, 2.0, 3.0, 4.0])
318                .expect("encode payload values");
319
320        let encoded = encode_tile(TileEncodeInput {
321            tile_id: 1004,
322            mesh_kind: MeshKind::JisX0410,
323            dtype: DType::Uint16,
324            endianness: Endianness::Little,
325            compression: CompressionMode::DeflateRaw,
326            dimensions: tile_dims(),
327            no_data: None,
328            payload: &payload,
329        })
330        .expect("encode tile");
331
332        let decoded = decode_tile_minimal(&encoded.bytes).expect("decode tile");
333        assert_eq!(decoded.header.compression, CompressionMode::DeflateRaw);
334        assert_eq!(decoded.payload, payload);
335    }
336
337    #[test]
338    fn rejects_invalid_magic() {
339        let payload =
340            encode_payload_values(DType::Uint8, Endianness::Little, &[1.0, 2.0, 3.0, 4.0])
341                .expect("encode payload values");
342
343        let encoded = encode_tile(TileEncodeInput {
344            tile_id: 1,
345            mesh_kind: MeshKind::JisX0410,
346            dtype: DType::Uint8,
347            endianness: Endianness::Little,
348            compression: CompressionMode::None,
349            dimensions: tile_dims(),
350            no_data: None,
351            payload: &payload,
352        })
353        .expect("encode tile");
354
355        let mut malformed = encoded.bytes;
356        malformed[1] = 0;
357        let error = decode_tile_minimal(&malformed).expect_err("should fail");
358        assert_eq!(error.code, TileErrorCode::InvalidMagic);
359    }
360
361    #[test]
362    fn rejects_invalid_xyz_tile_id() {
363        let payload =
364            encode_payload_values(DType::Uint8, Endianness::Little, &[1.0, 2.0, 3.0, 4.0])
365                .expect("encode payload values");
366
367        let bad_tile_id = (1_u64 << 58) | 16_u64;
368        let error = encode_tile(TileEncodeInput {
369            tile_id: bad_tile_id,
370            mesh_kind: MeshKind::Xyz,
371            dtype: DType::Uint8,
372            endianness: Endianness::Little,
373            compression: CompressionMode::None,
374            dimensions: tile_dims(),
375            no_data: None,
376            payload: &payload,
377        })
378        .expect_err("should reject bad xyz tile id");
379
380        assert_eq!(error.code, TileErrorCode::InvalidFieldValue);
381    }
382}