1use std::collections::HashSet;
11use std::error::Error;
12use std::fmt;
13
14pub const GT_MODEL_TYPE: u16 = 1024;
16pub const GT_RASTER_TYPE: u16 = 1025;
17pub const GT_CITATION: u16 = 1026;
18pub const GEODETIC_CRS_TYPE: u16 = 2048;
19pub const GEOGRAPHIC_TYPE: u16 = GEODETIC_CRS_TYPE;
20pub const GEODETIC_CITATION: u16 = 2049;
21pub const GEOG_CITATION: u16 = 2049;
22pub const GEODETIC_DATUM: u16 = 2050;
23pub const GEOG_GEODETIC_DATUM: u16 = 2050;
24pub const GEOG_ANGULAR_UNITS: u16 = 2054;
25pub const PROJECTED_CRS_TYPE: u16 = 3072;
26pub const PROJECTED_CS_TYPE: u16 = 3072;
27pub const PROJ_CITATION: u16 = 3073;
28pub const PROJECTION: u16 = 3074;
29pub const PROJ_COORD_TRANS: u16 = 3075;
30pub const PROJ_LINEAR_UNITS: u16 = 3076;
31pub const VERTICAL_CITATION: u16 = 4097;
32pub const VERTICAL_CS_TYPE: u16 = 4096;
33pub const VERTICAL_DATUM: u16 = 4098;
34pub const VERTICAL_UNITS: u16 = 4099;
35const GEO_DOUBLE_PARAMS_TAG: u16 = 34736;
36const GEO_ASCII_PARAMS_TAG: u16 = 34737;
37pub const GEO_KEY_DIRECTORY_VERSION: u16 = 1;
38pub const GEO_KEY_REVISION: u16 = 1;
39pub const GEO_KEY_MINOR_REVISION_1_0: u16 = 0;
40pub const GEO_KEY_MINOR_REVISION_1_1: u16 = 1;
41
42#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum GeoKeySerializeError {
46 InvalidDirectoryVersion { version: u16 },
48 InvalidMajorRevision { major_revision: u16 },
50 InvalidMinorRevision { minor_revision: u16 },
52 DuplicateKey { key_id: u16 },
54 InvalidAsciiValue { key_id: u16 },
56 TooManyKeys { count: usize },
58 ValueCountTooLarge { key_id: u16, tag: u16, count: usize },
60 ParameterOffsetTooLarge {
62 key_id: u16,
63 tag: u16,
64 offset: usize,
65 },
66}
67
68impl fmt::Display for GeoKeySerializeError {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 Self::InvalidDirectoryVersion { version } => write!(
72 f,
73 "GeoKey directory version {version} is invalid; expected 1"
74 ),
75 Self::InvalidMajorRevision { major_revision } => write!(
76 f,
77 "GeoKey directory major revision {major_revision} is invalid; expected 1"
78 ),
79 Self::InvalidMinorRevision { minor_revision } => write!(
80 f,
81 "GeoKey directory minor revision {minor_revision} is invalid; expected 0 or 1"
82 ),
83 Self::DuplicateKey { key_id } => {
84 write!(f, "GeoKey directory contains duplicate key ID {key_id}")
85 }
86 Self::InvalidAsciiValue { key_id } => write!(
87 f,
88 "GeoKey {key_id} ASCII value must contain only ASCII characters and no '|' delimiter"
89 ),
90 Self::TooManyKeys { count } => {
91 write!(
92 f,
93 "GeoKey directory contains {count} keys, exceeding u16::MAX"
94 )
95 }
96 Self::ValueCountTooLarge { key_id, tag, count } => write!(
97 f,
98 "GeoKey {key_id} references {count} values in tag {tag}, exceeding u16::MAX"
99 ),
100 Self::ParameterOffsetTooLarge {
101 key_id,
102 tag,
103 offset,
104 } => write!(
105 f,
106 "GeoKey {key_id} parameter offset {offset} in tag {tag} exceeds u16::MAX"
107 ),
108 }
109 }
110}
111
112impl Error for GeoKeySerializeError {}
113
114#[derive(Debug, Clone)]
116pub struct GeoKey {
117 pub id: u16,
118 pub value: GeoKeyValue,
119}
120
121#[derive(Debug, Clone)]
123pub enum GeoKeyValue {
124 Short(u16),
126 Double(Vec<f64>),
128 Ascii(String),
130}
131
132#[derive(Debug, Clone)]
134pub struct GeoKeyDirectory {
135 pub version: u16,
136 pub major_revision: u16,
137 pub minor_revision: u16,
138 pub keys: Vec<GeoKey>,
139}
140
141impl GeoKeyDirectory {
142 pub fn new() -> Self {
144 Self {
145 version: GEO_KEY_DIRECTORY_VERSION,
146 major_revision: GEO_KEY_REVISION,
147 minor_revision: GEO_KEY_MINOR_REVISION_1_1,
148 keys: Vec::new(),
149 }
150 }
151
152 pub fn parse(directory: &[u16], double_params: &[f64], ascii_params: &str) -> Option<Self> {
158 if directory.len() < 4 {
159 return None;
160 }
161
162 let version = directory[0];
163 let major_revision = directory[1];
164 let minor_revision = directory[2];
165 let num_keys = directory[3] as usize;
166
167 if version != GEO_KEY_DIRECTORY_VERSION
168 || major_revision != GEO_KEY_REVISION
169 || !matches!(
170 minor_revision,
171 GEO_KEY_MINOR_REVISION_1_0 | GEO_KEY_MINOR_REVISION_1_1
172 )
173 || directory.len() != 4 + num_keys * 4
174 || !ascii_params.is_ascii()
175 {
176 return None;
177 }
178
179 let mut keys = Vec::with_capacity(num_keys);
180 let mut seen_ids = HashSet::with_capacity(num_keys);
181 for i in 0..num_keys {
182 let base = 4 + i * 4;
183 let key_id = directory[base];
184 let location = directory[base + 1];
185 let count = directory[base + 2] as usize;
186 let value_offset = directory[base + 3];
187 if !seen_ids.insert(key_id) {
188 return None;
189 }
190
191 let value = match location {
192 0 => {
193 if count != 1 {
195 return None;
196 }
197 GeoKeyValue::Short(value_offset)
198 }
199 34736 => {
200 if count == 0 {
202 return None;
203 }
204 let start = value_offset as usize;
205 let end = start.checked_add(count)?;
206 if end <= double_params.len() {
207 GeoKeyValue::Double(double_params[start..end].to_vec())
208 } else {
209 return None;
210 }
211 }
212 34737 => {
213 if count == 0 {
215 return None;
216 }
217 let start = value_offset as usize;
218 let end = start.checked_add(count)?;
219 if let Some(raw) = ascii_params.get(start..end) {
220 if !raw.ends_with('|') {
221 return None;
222 }
223 let s = raw.trim_end_matches('|').trim_end_matches('\0').to_string();
224 GeoKeyValue::Ascii(s)
225 } else {
226 return None;
227 }
228 }
229 _ => return None,
230 };
231
232 keys.push(GeoKey { id: key_id, value });
233 }
234
235 Some(Self {
236 version,
237 major_revision,
238 minor_revision,
239 keys,
240 })
241 }
242
243 pub fn get(&self, id: u16) -> Option<&GeoKey> {
245 self.keys.iter().find(|k| k.id == id)
246 }
247
248 pub fn get_short(&self, id: u16) -> Option<u16> {
250 self.get(id).and_then(|k| match &k.value {
251 GeoKeyValue::Short(v) => Some(*v),
252 _ => None,
253 })
254 }
255
256 pub fn get_ascii(&self, id: u16) -> Option<&str> {
258 self.get(id).and_then(|k| match &k.value {
259 GeoKeyValue::Ascii(s) => Some(s.as_str()),
260 _ => None,
261 })
262 }
263
264 pub fn get_double(&self, id: u16) -> Option<&[f64]> {
266 self.get(id).and_then(|k| match &k.value {
267 GeoKeyValue::Double(v) => Some(v.as_slice()),
268 _ => None,
269 })
270 }
271
272 pub fn set(&mut self, id: u16, value: GeoKeyValue) {
274 if let Some(existing) = self.keys.iter_mut().find(|k| k.id == id) {
275 existing.value = value;
276 } else {
277 self.keys.push(GeoKey { id, value });
278 }
279 }
280
281 pub fn remove(&mut self, id: u16) {
283 self.keys.retain(|k| k.id != id);
284 }
285
286 pub fn serialize(&self) -> Result<(Vec<u16>, Vec<f64>, String), GeoKeySerializeError> {
293 if self.version != GEO_KEY_DIRECTORY_VERSION {
294 return Err(GeoKeySerializeError::InvalidDirectoryVersion {
295 version: self.version,
296 });
297 }
298 if self.major_revision != GEO_KEY_REVISION {
299 return Err(GeoKeySerializeError::InvalidMajorRevision {
300 major_revision: self.major_revision,
301 });
302 }
303
304 let mut sorted_keys = self.keys.clone();
305 sorted_keys.sort_by_key(|k| k.id);
306 if let Some(duplicate) = sorted_keys.windows(2).find(|keys| keys[0].id == keys[1].id) {
307 return Err(GeoKeySerializeError::DuplicateKey {
308 key_id: duplicate[0].id,
309 });
310 }
311 let key_count =
312 u16::try_from(sorted_keys.len()).map_err(|_| GeoKeySerializeError::TooManyKeys {
313 count: sorted_keys.len(),
314 })?;
315
316 let mut directory = Vec::new();
317 let mut double_params = Vec::new();
318 let mut ascii_params = String::new();
319
320 let minor_revision = self.serialized_minor_revision()?;
321
322 directory.push(self.version);
324 directory.push(self.major_revision);
325 directory.push(minor_revision);
326 directory.push(key_count);
327
328 for key in &sorted_keys {
329 directory.push(key.id);
330 match &key.value {
331 GeoKeyValue::Short(v) => {
332 directory.push(0); directory.push(1); directory.push(*v); }
336 GeoKeyValue::Double(v) => {
337 let count = checked_u16_len(key.id, GEO_DOUBLE_PARAMS_TAG, v.len())?;
338 let offset =
339 checked_u16_offset(key.id, GEO_DOUBLE_PARAMS_TAG, double_params.len())?;
340 directory.push(GEO_DOUBLE_PARAMS_TAG); directory.push(count);
342 directory.push(offset);
343 double_params.extend_from_slice(v);
344 }
345 GeoKeyValue::Ascii(s) => {
346 if !s.is_ascii() || s.contains('|') {
347 return Err(GeoKeySerializeError::InvalidAsciiValue { key_id: key.id });
348 }
349 let ascii_with_pipe = format!("{}|", s);
350 let count =
351 checked_u16_len(key.id, GEO_ASCII_PARAMS_TAG, ascii_with_pipe.len())?;
352 let offset =
353 checked_u16_offset(key.id, GEO_ASCII_PARAMS_TAG, ascii_params.len())?;
354 directory.push(GEO_ASCII_PARAMS_TAG); directory.push(count);
356 directory.push(offset);
357 ascii_params.push_str(&ascii_with_pipe);
358 }
359 }
360 }
361
362 Ok((directory, double_params, ascii_params))
363 }
364
365 fn serialized_minor_revision(&self) -> Result<u16, GeoKeySerializeError> {
366 match self.minor_revision {
367 GEO_KEY_MINOR_REVISION_1_0 if self.requires_geotiff_1_1() => {
368 Ok(GEO_KEY_MINOR_REVISION_1_1)
369 }
370 GEO_KEY_MINOR_REVISION_1_0 | GEO_KEY_MINOR_REVISION_1_1 => Ok(self.minor_revision),
371 minor_revision => Err(GeoKeySerializeError::InvalidMinorRevision { minor_revision }),
372 }
373 }
374
375 fn requires_geotiff_1_1(&self) -> bool {
376 self.keys.iter().any(|key| {
377 matches!(
378 key.id,
379 VERTICAL_CS_TYPE | VERTICAL_CITATION | VERTICAL_DATUM | VERTICAL_UNITS
380 )
381 })
382 }
383}
384
385fn checked_u16_len(key_id: u16, tag: u16, count: usize) -> Result<u16, GeoKeySerializeError> {
386 u16::try_from(count).map_err(|_| GeoKeySerializeError::ValueCountTooLarge {
387 key_id,
388 tag,
389 count,
390 })
391}
392
393fn checked_u16_offset(key_id: u16, tag: u16, offset: usize) -> Result<u16, GeoKeySerializeError> {
394 u16::try_from(offset).map_err(|_| GeoKeySerializeError::ParameterOffsetTooLarge {
395 key_id,
396 tag,
397 offset,
398 })
399}
400
401impl Default for GeoKeyDirectory {
402 fn default() -> Self {
403 Self::new()
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
412 fn parse_roundtrip() {
413 let mut dir = GeoKeyDirectory::new();
414 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
415 dir.set(GEOGRAPHIC_TYPE, GeoKeyValue::Short(4326));
416 dir.set(GEOG_CITATION, GeoKeyValue::Ascii("WGS 84".into()));
417
418 let (shorts, doubles, ascii) = dir.serialize().unwrap();
419 assert_eq!(shorts[..4], [1, 1, 1, 3]);
420 let parsed = GeoKeyDirectory::parse(&shorts, &doubles, &ascii).unwrap();
421
422 assert_eq!(parsed.get_short(GT_MODEL_TYPE), Some(2));
423 assert_eq!(parsed.get_short(GEOGRAPHIC_TYPE), Some(4326));
424 assert_eq!(parsed.get_ascii(GEOG_CITATION), Some("WGS 84"));
425 }
426
427 #[test]
428 fn serialize_preserves_legacy_minor_revision_zero_when_compatible() {
429 let mut dir = GeoKeyDirectory::new();
430 dir.minor_revision = GEO_KEY_MINOR_REVISION_1_0;
431 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
432
433 let (shorts, _, _) = dir.serialize().unwrap();
434 assert_eq!(shorts[..4], [1, 1, 0, 1]);
435 }
436
437 #[test]
438 fn serialize_promotes_vertical_geokeys_to_geotiff_1_1_minor_revision() {
439 let mut dir = GeoKeyDirectory::new();
440 dir.minor_revision = GEO_KEY_MINOR_REVISION_1_0;
441 dir.set(VERTICAL_CS_TYPE, GeoKeyValue::Short(5703));
442
443 let (shorts, _, _) = dir.serialize().unwrap();
444 assert_eq!(shorts[..4], [1, 1, 1, 1]);
445 }
446
447 #[test]
448 fn serialize_rejects_invalid_minor_revision() {
449 let mut dir = GeoKeyDirectory::new();
450 dir.minor_revision = 2;
451
452 let err = dir.serialize().unwrap_err();
453 assert_eq!(
454 err,
455 GeoKeySerializeError::InvalidMinorRevision { minor_revision: 2 }
456 );
457 }
458
459 #[test]
460 fn parse_rejects_invalid_headers_duplicate_keys_and_inline_counts() {
461 assert!(GeoKeyDirectory::parse(&[2, 1, 1, 0], &[], "").is_none());
462 assert!(GeoKeyDirectory::parse(&[1, 2, 1, 0], &[], "").is_none());
463 assert!(GeoKeyDirectory::parse(&[1, 1, 2, 0], &[], "").is_none());
464 assert!(GeoKeyDirectory::parse(&[1, 1, 1, 1, GT_MODEL_TYPE, 0, 2, 1], &[], "").is_none());
465 assert!(GeoKeyDirectory::parse(
466 &[1, 1, 1, 2, GT_MODEL_TYPE, 0, 1, 1, GT_MODEL_TYPE, 0, 1, 2],
467 &[],
468 ""
469 )
470 .is_none());
471 }
472
473 #[test]
474 fn serialize_rejects_invalid_headers_duplicates_and_non_ascii_values() {
475 let mut dir = GeoKeyDirectory::new();
476 dir.version = 2;
477 assert!(matches!(
478 dir.serialize(),
479 Err(GeoKeySerializeError::InvalidDirectoryVersion { version: 2 })
480 ));
481
482 let mut dir = GeoKeyDirectory::new();
483 dir.keys = vec![
484 GeoKey {
485 id: GT_MODEL_TYPE,
486 value: GeoKeyValue::Short(1),
487 },
488 GeoKey {
489 id: GT_MODEL_TYPE,
490 value: GeoKeyValue::Short(2),
491 },
492 ];
493 assert!(matches!(
494 dir.serialize(),
495 Err(GeoKeySerializeError::DuplicateKey {
496 key_id: GT_MODEL_TYPE
497 })
498 ));
499
500 let mut dir = GeoKeyDirectory::new();
501 dir.set(GEOG_CITATION, GeoKeyValue::Ascii("WGS 84 | invalid".into()));
502 assert!(matches!(
503 dir.serialize(),
504 Err(GeoKeySerializeError::InvalidAsciiValue {
505 key_id: GEOG_CITATION
506 })
507 ));
508 }
509
510 #[test]
511 fn set_replaces_existing() {
512 let mut dir = GeoKeyDirectory::new();
513 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
514 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
515 assert_eq!(dir.get_short(GT_MODEL_TYPE), Some(2));
516 assert_eq!(dir.keys.len(), 1);
517 }
518
519 #[test]
520 fn remove_key() {
521 let mut dir = GeoKeyDirectory::new();
522 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
523 dir.remove(GT_MODEL_TYPE);
524 assert!(dir.get(GT_MODEL_TYPE).is_none());
525 }
526
527 #[test]
528 fn parse_rejects_invalid_parameter_references_without_panicking() {
529 let directory = [
530 1u16,
531 1,
532 0,
533 1, GEOG_CITATION,
535 34737,
536 1,
537 1, ];
539 let ascii = String::from_utf8_lossy(&[0xff, b'|']).into_owned();
540
541 assert!(GeoKeyDirectory::parse(&directory, &[], &ascii).is_none());
542 assert!(
543 GeoKeyDirectory::parse(&[1, 1, 1, 1, GEOG_CITATION, 34737, 2, 0], &[], "x|").is_some()
544 );
545 assert!(
546 GeoKeyDirectory::parse(&[1, 1, 1, 1, GEOG_CITATION, 34737, 1, 0], &[], "x").is_none()
547 );
548 assert!(
549 GeoKeyDirectory::parse(&[1, 1, 1, 1, GEOG_CITATION, 65000, 1, 0], &[], "").is_none()
550 );
551 }
552
553 #[test]
554 fn serialize_rejects_too_many_keys() {
555 let mut dir = GeoKeyDirectory::new();
556 dir.keys = (0..=u16::MAX as usize)
557 .map(|index| GeoKey {
558 id: index as u16,
559 value: GeoKeyValue::Short(1),
560 })
561 .collect();
562
563 let err = dir.serialize().unwrap_err();
564 assert_eq!(
565 err,
566 GeoKeySerializeError::TooManyKeys {
567 count: u16::MAX as usize + 1
568 }
569 );
570 }
571
572 #[test]
573 fn serialize_rejects_oversized_double_value_count() {
574 let mut dir = GeoKeyDirectory::new();
575 dir.set(
576 GT_CITATION,
577 GeoKeyValue::Double(vec![1.0; u16::MAX as usize + 1]),
578 );
579
580 let err = dir.serialize().unwrap_err();
581 assert_eq!(
582 err,
583 GeoKeySerializeError::ValueCountTooLarge {
584 key_id: GT_CITATION,
585 tag: GEO_DOUBLE_PARAMS_TAG,
586 count: u16::MAX as usize + 1
587 }
588 );
589 }
590
591 #[test]
592 fn serialize_rejects_oversized_ascii_parameter_offset() {
593 let mut dir = GeoKeyDirectory::new();
594 dir.set(
595 GEOG_CITATION,
596 GeoKeyValue::Ascii("a".repeat(u16::MAX as usize - 1)),
597 );
598 dir.set(PROJ_CITATION, GeoKeyValue::Ascii("b".to_string()));
599 dir.set(VERTICAL_CITATION, GeoKeyValue::Ascii("c".to_string()));
600
601 let err = dir.serialize().unwrap_err();
602 assert_eq!(
603 err,
604 GeoKeySerializeError::ParameterOffsetTooLarge {
605 key_id: VERTICAL_CITATION,
606 tag: GEO_ASCII_PARAMS_TAG,
607 offset: u16::MAX as usize + 2
608 }
609 );
610 }
611}