1use std::error::Error;
11use std::fmt;
12
13pub const GT_MODEL_TYPE: u16 = 1024;
15pub const GT_RASTER_TYPE: u16 = 1025;
16pub const GT_CITATION: u16 = 1026;
17pub const GEODETIC_CRS_TYPE: u16 = 2048;
18pub const GEOGRAPHIC_TYPE: u16 = GEODETIC_CRS_TYPE;
19pub const GEODETIC_CITATION: u16 = 2049;
20pub const GEOG_CITATION: u16 = 2049;
21pub const GEODETIC_DATUM: u16 = 2050;
22pub const GEOG_GEODETIC_DATUM: u16 = 2050;
23pub const GEOG_ANGULAR_UNITS: u16 = 2054;
24pub const PROJECTED_CRS_TYPE: u16 = 3072;
25pub const PROJECTED_CS_TYPE: u16 = 3072;
26pub const PROJ_CITATION: u16 = 3073;
27pub const PROJECTION: u16 = 3074;
28pub const PROJ_COORD_TRANS: u16 = 3075;
29pub const PROJ_LINEAR_UNITS: u16 = 3076;
30pub const VERTICAL_CITATION: u16 = 4097;
31pub const VERTICAL_CS_TYPE: u16 = 4096;
32pub const VERTICAL_DATUM: u16 = 4098;
33pub const VERTICAL_UNITS: u16 = 4099;
34const GEO_DOUBLE_PARAMS_TAG: u16 = 34736;
35const GEO_ASCII_PARAMS_TAG: u16 = 34737;
36pub const GEO_KEY_DIRECTORY_VERSION: u16 = 1;
37pub const GEO_KEY_REVISION: u16 = 1;
38pub const GEO_KEY_MINOR_REVISION_1_0: u16 = 0;
39pub const GEO_KEY_MINOR_REVISION_1_1: u16 = 1;
40
41#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum GeoKeySerializeError {
45 InvalidMinorRevision { minor_revision: u16 },
47 TooManyKeys { count: usize },
49 ValueCountTooLarge { key_id: u16, tag: u16, count: usize },
51 ParameterOffsetTooLarge {
53 key_id: u16,
54 tag: u16,
55 offset: usize,
56 },
57}
58
59impl fmt::Display for GeoKeySerializeError {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 Self::InvalidMinorRevision { minor_revision } => write!(
63 f,
64 "GeoKey directory minor revision {minor_revision} is invalid; expected 0 or 1"
65 ),
66 Self::TooManyKeys { count } => {
67 write!(
68 f,
69 "GeoKey directory contains {count} keys, exceeding u16::MAX"
70 )
71 }
72 Self::ValueCountTooLarge { key_id, tag, count } => write!(
73 f,
74 "GeoKey {key_id} references {count} values in tag {tag}, exceeding u16::MAX"
75 ),
76 Self::ParameterOffsetTooLarge {
77 key_id,
78 tag,
79 offset,
80 } => write!(
81 f,
82 "GeoKey {key_id} parameter offset {offset} in tag {tag} exceeds u16::MAX"
83 ),
84 }
85 }
86}
87
88impl Error for GeoKeySerializeError {}
89
90#[derive(Debug, Clone)]
92pub struct GeoKey {
93 pub id: u16,
94 pub value: GeoKeyValue,
95}
96
97#[derive(Debug, Clone)]
99pub enum GeoKeyValue {
100 Short(u16),
102 Double(Vec<f64>),
104 Ascii(String),
106}
107
108#[derive(Debug, Clone)]
110pub struct GeoKeyDirectory {
111 pub version: u16,
112 pub major_revision: u16,
113 pub minor_revision: u16,
114 pub keys: Vec<GeoKey>,
115}
116
117impl GeoKeyDirectory {
118 pub fn new() -> Self {
120 Self {
121 version: GEO_KEY_DIRECTORY_VERSION,
122 major_revision: GEO_KEY_REVISION,
123 minor_revision: GEO_KEY_MINOR_REVISION_1_1,
124 keys: Vec::new(),
125 }
126 }
127
128 pub fn parse(directory: &[u16], double_params: &[f64], ascii_params: &str) -> Option<Self> {
134 if directory.len() < 4 {
135 return None;
136 }
137
138 let version = directory[0];
139 let major_revision = directory[1];
140 let minor_revision = directory[2];
141 let num_keys = directory[3] as usize;
142
143 if directory.len() < 4 + num_keys * 4 {
144 return None;
145 }
146
147 let mut keys = Vec::with_capacity(num_keys);
148 for i in 0..num_keys {
149 let base = 4 + i * 4;
150 let key_id = directory[base];
151 let location = directory[base + 1];
152 let count = directory[base + 2] as usize;
153 let value_offset = directory[base + 3];
154
155 let value = match location {
156 0 => {
157 GeoKeyValue::Short(value_offset)
159 }
160 34736 => {
161 let start = value_offset as usize;
163 let end = start + count;
164 if end <= double_params.len() {
165 GeoKeyValue::Double(double_params[start..end].to_vec())
166 } else {
167 continue;
168 }
169 }
170 34737 => {
171 let start = value_offset as usize;
173 let end = start + count;
174 if let Some(raw) = ascii_params.get(start..end) {
175 let s = raw.trim_end_matches('|').trim_end_matches('\0').to_string();
176 GeoKeyValue::Ascii(s)
177 } else {
178 continue;
179 }
180 }
181 _ => continue,
182 };
183
184 keys.push(GeoKey { id: key_id, value });
185 }
186
187 Some(Self {
188 version,
189 major_revision,
190 minor_revision,
191 keys,
192 })
193 }
194
195 pub fn get(&self, id: u16) -> Option<&GeoKey> {
197 self.keys.iter().find(|k| k.id == id)
198 }
199
200 pub fn get_short(&self, id: u16) -> Option<u16> {
202 self.get(id).and_then(|k| match &k.value {
203 GeoKeyValue::Short(v) => Some(*v),
204 _ => None,
205 })
206 }
207
208 pub fn get_ascii(&self, id: u16) -> Option<&str> {
210 self.get(id).and_then(|k| match &k.value {
211 GeoKeyValue::Ascii(s) => Some(s.as_str()),
212 _ => None,
213 })
214 }
215
216 pub fn get_double(&self, id: u16) -> Option<&[f64]> {
218 self.get(id).and_then(|k| match &k.value {
219 GeoKeyValue::Double(v) => Some(v.as_slice()),
220 _ => None,
221 })
222 }
223
224 pub fn set(&mut self, id: u16, value: GeoKeyValue) {
226 if let Some(existing) = self.keys.iter_mut().find(|k| k.id == id) {
227 existing.value = value;
228 } else {
229 self.keys.push(GeoKey { id, value });
230 }
231 }
232
233 pub fn remove(&mut self, id: u16) {
235 self.keys.retain(|k| k.id != id);
236 }
237
238 pub fn serialize(&self) -> Result<(Vec<u16>, Vec<f64>, String), GeoKeySerializeError> {
245 let mut sorted_keys = self.keys.clone();
246 sorted_keys.sort_by_key(|k| k.id);
247 let key_count =
248 u16::try_from(sorted_keys.len()).map_err(|_| GeoKeySerializeError::TooManyKeys {
249 count: sorted_keys.len(),
250 })?;
251
252 let mut directory = Vec::new();
253 let mut double_params = Vec::new();
254 let mut ascii_params = String::new();
255
256 let minor_revision = self.serialized_minor_revision()?;
257
258 directory.push(self.version);
260 directory.push(self.major_revision);
261 directory.push(minor_revision);
262 directory.push(key_count);
263
264 for key in &sorted_keys {
265 directory.push(key.id);
266 match &key.value {
267 GeoKeyValue::Short(v) => {
268 directory.push(0); directory.push(1); directory.push(*v); }
272 GeoKeyValue::Double(v) => {
273 let count = checked_u16_len(key.id, GEO_DOUBLE_PARAMS_TAG, v.len())?;
274 let offset =
275 checked_u16_offset(key.id, GEO_DOUBLE_PARAMS_TAG, double_params.len())?;
276 directory.push(GEO_DOUBLE_PARAMS_TAG); directory.push(count);
278 directory.push(offset);
279 double_params.extend_from_slice(v);
280 }
281 GeoKeyValue::Ascii(s) => {
282 let ascii_with_pipe = format!("{}|", s);
283 let count =
284 checked_u16_len(key.id, GEO_ASCII_PARAMS_TAG, ascii_with_pipe.len())?;
285 let offset =
286 checked_u16_offset(key.id, GEO_ASCII_PARAMS_TAG, ascii_params.len())?;
287 directory.push(GEO_ASCII_PARAMS_TAG); directory.push(count);
289 directory.push(offset);
290 ascii_params.push_str(&ascii_with_pipe);
291 }
292 }
293 }
294
295 Ok((directory, double_params, ascii_params))
296 }
297
298 fn serialized_minor_revision(&self) -> Result<u16, GeoKeySerializeError> {
299 match self.minor_revision {
300 GEO_KEY_MINOR_REVISION_1_0 if self.requires_geotiff_1_1() => {
301 Ok(GEO_KEY_MINOR_REVISION_1_1)
302 }
303 GEO_KEY_MINOR_REVISION_1_0 | GEO_KEY_MINOR_REVISION_1_1 => Ok(self.minor_revision),
304 minor_revision => Err(GeoKeySerializeError::InvalidMinorRevision { minor_revision }),
305 }
306 }
307
308 fn requires_geotiff_1_1(&self) -> bool {
309 self.keys.iter().any(|key| {
310 matches!(
311 key.id,
312 VERTICAL_CS_TYPE | VERTICAL_CITATION | VERTICAL_DATUM | VERTICAL_UNITS
313 )
314 })
315 }
316}
317
318fn checked_u16_len(key_id: u16, tag: u16, count: usize) -> Result<u16, GeoKeySerializeError> {
319 u16::try_from(count).map_err(|_| GeoKeySerializeError::ValueCountTooLarge {
320 key_id,
321 tag,
322 count,
323 })
324}
325
326fn checked_u16_offset(key_id: u16, tag: u16, offset: usize) -> Result<u16, GeoKeySerializeError> {
327 u16::try_from(offset).map_err(|_| GeoKeySerializeError::ParameterOffsetTooLarge {
328 key_id,
329 tag,
330 offset,
331 })
332}
333
334impl Default for GeoKeyDirectory {
335 fn default() -> Self {
336 Self::new()
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn parse_roundtrip() {
346 let mut dir = GeoKeyDirectory::new();
347 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
348 dir.set(GEOGRAPHIC_TYPE, GeoKeyValue::Short(4326));
349 dir.set(GEOG_CITATION, GeoKeyValue::Ascii("WGS 84".into()));
350
351 let (shorts, doubles, ascii) = dir.serialize().unwrap();
352 assert_eq!(shorts[..4], [1, 1, 1, 3]);
353 let parsed = GeoKeyDirectory::parse(&shorts, &doubles, &ascii).unwrap();
354
355 assert_eq!(parsed.get_short(GT_MODEL_TYPE), Some(2));
356 assert_eq!(parsed.get_short(GEOGRAPHIC_TYPE), Some(4326));
357 assert_eq!(parsed.get_ascii(GEOG_CITATION), Some("WGS 84"));
358 }
359
360 #[test]
361 fn serialize_preserves_legacy_minor_revision_zero_when_compatible() {
362 let mut dir = GeoKeyDirectory::new();
363 dir.minor_revision = GEO_KEY_MINOR_REVISION_1_0;
364 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
365
366 let (shorts, _, _) = dir.serialize().unwrap();
367 assert_eq!(shorts[..4], [1, 1, 0, 1]);
368 }
369
370 #[test]
371 fn serialize_promotes_vertical_geokeys_to_geotiff_1_1_minor_revision() {
372 let mut dir = GeoKeyDirectory::new();
373 dir.minor_revision = GEO_KEY_MINOR_REVISION_1_0;
374 dir.set(VERTICAL_CS_TYPE, GeoKeyValue::Short(5703));
375
376 let (shorts, _, _) = dir.serialize().unwrap();
377 assert_eq!(shorts[..4], [1, 1, 1, 1]);
378 }
379
380 #[test]
381 fn serialize_rejects_invalid_minor_revision() {
382 let mut dir = GeoKeyDirectory::new();
383 dir.minor_revision = 2;
384
385 let err = dir.serialize().unwrap_err();
386 assert_eq!(
387 err,
388 GeoKeySerializeError::InvalidMinorRevision { minor_revision: 2 }
389 );
390 }
391
392 #[test]
393 fn set_replaces_existing() {
394 let mut dir = GeoKeyDirectory::new();
395 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
396 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
397 assert_eq!(dir.get_short(GT_MODEL_TYPE), Some(2));
398 assert_eq!(dir.keys.len(), 1);
399 }
400
401 #[test]
402 fn remove_key() {
403 let mut dir = GeoKeyDirectory::new();
404 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
405 dir.remove(GT_MODEL_TYPE);
406 assert!(dir.get(GT_MODEL_TYPE).is_none());
407 }
408
409 #[test]
410 fn parse_skips_invalid_ascii_subslice_without_panicking() {
411 let directory = [
412 1u16,
413 1,
414 0,
415 1, GEOG_CITATION,
417 34737,
418 1,
419 1, ];
421 let ascii = String::from_utf8_lossy(&[0xff, b'|']).into_owned();
422
423 let parsed = GeoKeyDirectory::parse(&directory, &[], &ascii).unwrap();
424 assert!(parsed.get_ascii(GEOG_CITATION).is_none());
425 }
426
427 #[test]
428 fn serialize_rejects_too_many_keys() {
429 let mut dir = GeoKeyDirectory::new();
430 dir.keys = (0..=u16::MAX as usize)
431 .map(|index| GeoKey {
432 id: index as u16,
433 value: GeoKeyValue::Short(1),
434 })
435 .collect();
436
437 let err = dir.serialize().unwrap_err();
438 assert_eq!(
439 err,
440 GeoKeySerializeError::TooManyKeys {
441 count: u16::MAX as usize + 1
442 }
443 );
444 }
445
446 #[test]
447 fn serialize_rejects_oversized_double_value_count() {
448 let mut dir = GeoKeyDirectory::new();
449 dir.set(
450 GT_CITATION,
451 GeoKeyValue::Double(vec![1.0; u16::MAX as usize + 1]),
452 );
453
454 let err = dir.serialize().unwrap_err();
455 assert_eq!(
456 err,
457 GeoKeySerializeError::ValueCountTooLarge {
458 key_id: GT_CITATION,
459 tag: GEO_DOUBLE_PARAMS_TAG,
460 count: u16::MAX as usize + 1
461 }
462 );
463 }
464
465 #[test]
466 fn serialize_rejects_oversized_ascii_parameter_offset() {
467 let mut dir = GeoKeyDirectory::new();
468 dir.set(
469 GEOG_CITATION,
470 GeoKeyValue::Ascii("a".repeat(u16::MAX as usize - 1)),
471 );
472 dir.set(PROJ_CITATION, GeoKeyValue::Ascii("b".to_string()));
473 dir.set(VERTICAL_CITATION, GeoKeyValue::Ascii("c".to_string()));
474
475 let err = dir.serialize().unwrap_err();
476 assert_eq!(
477 err,
478 GeoKeySerializeError::ParameterOffsetTooLarge {
479 key_id: VERTICAL_CITATION,
480 tag: GEO_ASCII_PARAMS_TAG,
481 offset: u16::MAX as usize + 2
482 }
483 );
484 }
485}