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}