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, Default)]
100pub enum CompressionMode {
101    #[default]
102    None,
103    DeflateRaw,
104}
105
106impl CompressionMode {
107    pub(crate) fn code(self) -> u8 {
108        match self {
109            Self::None => 0,
110            Self::DeflateRaw => 1,
111        }
112    }
113
114    pub(crate) fn from_code(code: u8) -> Result<Self> {
115        match code {
116            0 => Ok(Self::None),
117            1 => Ok(Self::DeflateRaw),
118            _ => Err(TileError::new(
119                TileErrorCode::InvalidFieldValue,
120                format!("Invalid compression code {code}."),
121            )),
122        }
123    }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct TileDimensions {
128    pub rows: u32,
129    pub cols: u32,
130    pub bands: u8,
131}
132
133impl TileDimensions {
134    pub(crate) fn validate(self) -> Result<()> {
135        if self.rows == 0 || self.cols == 0 || self.bands == 0 {
136            return Err(TileError::new(
137                TileErrorCode::InvalidFieldValue,
138                "rows, cols, and bands must be > 0.",
139            ));
140        }
141        Ok(())
142    }
143
144    pub(crate) fn total_samples(self) -> Result<u64> {
145        let rows = u64::from(self.rows);
146        let cols = u64::from(self.cols);
147        let bands = u64::from(self.bands);
148        rows.checked_mul(cols)
149            .and_then(|v| v.checked_mul(bands))
150            .ok_or_else(|| {
151                TileError::new(
152                    TileErrorCode::InvalidFieldValue,
153                    "Invalid dimensions resulting in overflowed sample count.",
154                )
155            })
156    }
157}
158
159#[derive(Debug, Clone, PartialEq)]
160pub struct TileHeader {
161    pub format_major: u8,
162    pub tile_id: u64,
163    pub mesh_kind: MeshKind,
164    pub dtype: DType,
165    pub endianness: Endianness,
166    pub compression: CompressionMode,
167    pub dimensions: TileDimensions,
168    pub no_data_kind: u8,
169    pub no_data_value_raw: [u8; 8],
170    pub no_data: Option<f64>,
171    pub payload_uncompressed_bytes: u64,
172    pub payload_compressed_bytes: u64,
173    pub payload_crc32: u32,
174    pub header_crc32: u32,
175}
176
177#[derive(Debug, Clone)]
178pub struct TileEncodeInput<'a> {
179    pub tile_id: u64,
180    pub mesh_kind: MeshKind,
181    pub dtype: DType,
182    pub endianness: Endianness,
183    pub compression: CompressionMode,
184    pub dimensions: TileDimensions,
185    pub no_data: Option<f64>,
186    pub payload: &'a [u8],
187}
188
189#[derive(Debug, Clone, PartialEq)]
190pub struct EncodedTile {
191    pub bytes: Vec<u8>,
192    pub header: TileHeader,
193}
194
195#[derive(Debug, Clone, PartialEq)]
196pub struct DecodedTile {
197    pub header: TileHeader,
198    pub payload: Vec<u8>,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum TileErrorCode {
203    InvalidMagic,
204    UnsupportedVersion,
205    InvalidHeaderLength,
206    InvalidFieldValue,
207    MissingRequiredField,
208    HeaderChecksumMismatch,
209    InvalidPayloadLength,
210    UnsupportedCompression,
211    CompressionFailed,
212    DecompressionFailed,
213    PayloadChecksumMismatch,
214}
215
216impl TileErrorCode {
217    pub fn as_str(self) -> &'static str {
218        match self {
219            Self::InvalidMagic => "INVALID_MAGIC",
220            Self::UnsupportedVersion => "UNSUPPORTED_VERSION",
221            Self::InvalidHeaderLength => "INVALID_HEADER_LENGTH",
222            Self::InvalidFieldValue => "INVALID_FIELD_VALUE",
223            Self::MissingRequiredField => "MISSING_REQUIRED_FIELD",
224            Self::HeaderChecksumMismatch => "HEADER_CHECKSUM_MISMATCH",
225            Self::InvalidPayloadLength => "INVALID_PAYLOAD_LENGTH",
226            Self::UnsupportedCompression => "UNSUPPORTED_COMPRESSION",
227            Self::CompressionFailed => "COMPRESSION_FAILED",
228            Self::DecompressionFailed => "DECOMPRESSION_FAILED",
229            Self::PayloadChecksumMismatch => "PAYLOAD_CHECKSUM_MISMATCH",
230        }
231    }
232}
233
234impl fmt::Display for TileErrorCode {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        write!(f, "{}", self.as_str())
237    }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct TileError {
242    pub code: TileErrorCode,
243    pub message: String,
244}
245
246impl TileError {
247    pub fn new(code: TileErrorCode, message: impl Into<String>) -> Self {
248        Self {
249            code,
250            message: message.into(),
251        }
252    }
253}
254
255impl fmt::Display for TileError {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        write!(f, "{}: {}", self.code, self.message)
258    }
259}
260
261impl std::error::Error for TileError {}
262
263pub type Result<T> = std::result::Result<T, TileError>;
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    fn tile_dims() -> TileDimensions {
270        TileDimensions {
271            rows: 2,
272            cols: 2,
273            bands: 1,
274        }
275    }
276
277    #[test]
278    fn roundtrip_uncompressed_payload() {
279        let payload =
280            encode_payload_values(DType::Uint16, Endianness::Little, &[10.0, 20.0, 30.0, 40.0])
281                .expect("encode payload values");
282
283        let encoded = encode_tile(TileEncodeInput {
284            tile_id: 42,
285            mesh_kind: MeshKind::JisX0410,
286            dtype: DType::Uint16,
287            endianness: Endianness::Little,
288            compression: CompressionMode::None,
289            dimensions: tile_dims(),
290            no_data: None,
291            payload: &payload,
292        })
293        .expect("encode tile");
294
295        let decoded = decode_tile_minimal(&encoded.bytes).expect("decode tile");
296        assert_eq!(decoded.header.tile_id, 42);
297        assert_eq!(decoded.header.compression, CompressionMode::None);
298        assert_eq!(decoded.payload, payload);
299
300        let values = decode_payload_values(
301            decoded.header.dtype,
302            decoded.header.endianness,
303            &decoded.payload,
304            decoded.header.no_data,
305        )
306        .expect("decode payload values");
307        assert_eq!(values, vec![Some(10.0), Some(20.0), Some(30.0), Some(40.0)]);
308    }
309
310    #[test]
311    fn roundtrip_deflate_payload() {
312        let payload =
313            encode_payload_values(DType::Uint16, Endianness::Little, &[1.0, 2.0, 3.0, 4.0])
314                .expect("encode payload values");
315
316        let encoded = encode_tile(TileEncodeInput {
317            tile_id: 1004,
318            mesh_kind: MeshKind::JisX0410,
319            dtype: DType::Uint16,
320            endianness: Endianness::Little,
321            compression: CompressionMode::DeflateRaw,
322            dimensions: tile_dims(),
323            no_data: None,
324            payload: &payload,
325        })
326        .expect("encode tile");
327
328        let decoded = decode_tile_minimal(&encoded.bytes).expect("decode tile");
329        assert_eq!(decoded.header.compression, CompressionMode::DeflateRaw);
330        assert_eq!(decoded.payload, payload);
331    }
332
333    #[test]
334    fn rejects_invalid_magic() {
335        let payload =
336            encode_payload_values(DType::Uint8, Endianness::Little, &[1.0, 2.0, 3.0, 4.0])
337                .expect("encode payload values");
338
339        let encoded = encode_tile(TileEncodeInput {
340            tile_id: 1,
341            mesh_kind: MeshKind::JisX0410,
342            dtype: DType::Uint8,
343            endianness: Endianness::Little,
344            compression: CompressionMode::None,
345            dimensions: tile_dims(),
346            no_data: None,
347            payload: &payload,
348        })
349        .expect("encode tile");
350
351        let mut malformed = encoded.bytes;
352        malformed[1] = 0;
353        let error = decode_tile_minimal(&malformed).expect_err("should fail");
354        assert_eq!(error.code, TileErrorCode::InvalidMagic);
355    }
356
357    #[test]
358    fn rejects_invalid_xyz_tile_id() {
359        let payload =
360            encode_payload_values(DType::Uint8, Endianness::Little, &[1.0, 2.0, 3.0, 4.0])
361                .expect("encode payload values");
362
363        let bad_tile_id = (1_u64 << 58) | 16_u64;
364        let error = encode_tile(TileEncodeInput {
365            tile_id: bad_tile_id,
366            mesh_kind: MeshKind::Xyz,
367            dtype: DType::Uint8,
368            endianness: Endianness::Little,
369            compression: CompressionMode::None,
370            dimensions: tile_dims(),
371            no_data: None,
372            payload: &payload,
373        })
374        .expect_err("should reject bad xyz tile id");
375
376        assert_eq!(error.code, TileErrorCode::InvalidFieldValue);
377    }
378
379    #[test]
380    fn decodes_no_data_samples_to_none() {
381        let payload =
382            encode_payload_values(DType::Uint16, Endianness::Little, &[10.0, 20.0, 30.0, 20.0])
383                .expect("encode payload values");
384
385        let encoded = encode_tile(TileEncodeInput {
386            tile_id: 2001,
387            mesh_kind: MeshKind::JisX0410,
388            dtype: DType::Uint16,
389            endianness: Endianness::Little,
390            compression: CompressionMode::None,
391            dimensions: tile_dims(),
392            no_data: Some(20.0),
393            payload: &payload,
394        })
395        .expect("encode tile");
396
397        let decoded = decode_tile_minimal(&encoded.bytes).expect("decode tile");
398        let values = decode_payload_values(
399            decoded.header.dtype,
400            decoded.header.endianness,
401            &decoded.payload,
402            decoded.header.no_data,
403        )
404        .expect("decode payload values");
405
406        assert_eq!(values, vec![Some(10.0), None, Some(30.0), None]);
407    }
408}