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;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum GeoKeySerializeError {
41 TooManyKeys { count: usize },
43 ValueCountTooLarge { key_id: u16, tag: u16, count: usize },
45 ParameterOffsetTooLarge {
47 key_id: u16,
48 tag: u16,
49 offset: usize,
50 },
51}
52
53impl fmt::Display for GeoKeySerializeError {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 match self {
56 Self::TooManyKeys { count } => {
57 write!(
58 f,
59 "GeoKey directory contains {count} keys, exceeding u16::MAX"
60 )
61 }
62 Self::ValueCountTooLarge { key_id, tag, count } => write!(
63 f,
64 "GeoKey {key_id} references {count} values in tag {tag}, exceeding u16::MAX"
65 ),
66 Self::ParameterOffsetTooLarge {
67 key_id,
68 tag,
69 offset,
70 } => write!(
71 f,
72 "GeoKey {key_id} parameter offset {offset} in tag {tag} exceeds u16::MAX"
73 ),
74 }
75 }
76}
77
78impl Error for GeoKeySerializeError {}
79
80#[derive(Debug, Clone)]
82pub struct GeoKey {
83 pub id: u16,
84 pub value: GeoKeyValue,
85}
86
87#[derive(Debug, Clone)]
89pub enum GeoKeyValue {
90 Short(u16),
92 Double(Vec<f64>),
94 Ascii(String),
96}
97
98#[derive(Debug, Clone)]
100pub struct GeoKeyDirectory {
101 pub version: u16,
102 pub major_revision: u16,
103 pub minor_revision: u16,
104 pub keys: Vec<GeoKey>,
105}
106
107impl GeoKeyDirectory {
108 pub fn new() -> Self {
110 Self {
111 version: 1,
112 major_revision: 1,
113 minor_revision: 0,
114 keys: Vec::new(),
115 }
116 }
117
118 pub fn parse(directory: &[u16], double_params: &[f64], ascii_params: &str) -> Option<Self> {
124 if directory.len() < 4 {
125 return None;
126 }
127
128 let version = directory[0];
129 let major_revision = directory[1];
130 let minor_revision = directory[2];
131 let num_keys = directory[3] as usize;
132
133 if directory.len() < 4 + num_keys * 4 {
134 return None;
135 }
136
137 let mut keys = Vec::with_capacity(num_keys);
138 for i in 0..num_keys {
139 let base = 4 + i * 4;
140 let key_id = directory[base];
141 let location = directory[base + 1];
142 let count = directory[base + 2] as usize;
143 let value_offset = directory[base + 3];
144
145 let value = match location {
146 0 => {
147 GeoKeyValue::Short(value_offset)
149 }
150 34736 => {
151 let start = value_offset as usize;
153 let end = start + count;
154 if end <= double_params.len() {
155 GeoKeyValue::Double(double_params[start..end].to_vec())
156 } else {
157 continue;
158 }
159 }
160 34737 => {
161 let start = value_offset as usize;
163 let end = start + count;
164 if let Some(raw) = ascii_params.get(start..end) {
165 let s = raw.trim_end_matches('|').trim_end_matches('\0').to_string();
166 GeoKeyValue::Ascii(s)
167 } else {
168 continue;
169 }
170 }
171 _ => continue,
172 };
173
174 keys.push(GeoKey { id: key_id, value });
175 }
176
177 Some(Self {
178 version,
179 major_revision,
180 minor_revision,
181 keys,
182 })
183 }
184
185 pub fn get(&self, id: u16) -> Option<&GeoKey> {
187 self.keys.iter().find(|k| k.id == id)
188 }
189
190 pub fn get_short(&self, id: u16) -> Option<u16> {
192 self.get(id).and_then(|k| match &k.value {
193 GeoKeyValue::Short(v) => Some(*v),
194 _ => None,
195 })
196 }
197
198 pub fn get_ascii(&self, id: u16) -> Option<&str> {
200 self.get(id).and_then(|k| match &k.value {
201 GeoKeyValue::Ascii(s) => Some(s.as_str()),
202 _ => None,
203 })
204 }
205
206 pub fn get_double(&self, id: u16) -> Option<&[f64]> {
208 self.get(id).and_then(|k| match &k.value {
209 GeoKeyValue::Double(v) => Some(v.as_slice()),
210 _ => None,
211 })
212 }
213
214 pub fn set(&mut self, id: u16, value: GeoKeyValue) {
216 if let Some(existing) = self.keys.iter_mut().find(|k| k.id == id) {
217 existing.value = value;
218 } else {
219 self.keys.push(GeoKey { id, value });
220 }
221 }
222
223 pub fn remove(&mut self, id: u16) {
225 self.keys.retain(|k| k.id != id);
226 }
227
228 pub fn serialize(&self) -> Result<(Vec<u16>, Vec<f64>, String), GeoKeySerializeError> {
235 let mut sorted_keys = self.keys.clone();
236 sorted_keys.sort_by_key(|k| k.id);
237 let key_count =
238 u16::try_from(sorted_keys.len()).map_err(|_| GeoKeySerializeError::TooManyKeys {
239 count: sorted_keys.len(),
240 })?;
241
242 let mut directory = Vec::new();
243 let mut double_params = Vec::new();
244 let mut ascii_params = String::new();
245
246 directory.push(self.version);
248 directory.push(self.major_revision);
249 directory.push(self.minor_revision);
250 directory.push(key_count);
251
252 for key in &sorted_keys {
253 directory.push(key.id);
254 match &key.value {
255 GeoKeyValue::Short(v) => {
256 directory.push(0); directory.push(1); directory.push(*v); }
260 GeoKeyValue::Double(v) => {
261 let count = checked_u16_len(key.id, GEO_DOUBLE_PARAMS_TAG, v.len())?;
262 let offset =
263 checked_u16_offset(key.id, GEO_DOUBLE_PARAMS_TAG, double_params.len())?;
264 directory.push(GEO_DOUBLE_PARAMS_TAG); directory.push(count);
266 directory.push(offset);
267 double_params.extend_from_slice(v);
268 }
269 GeoKeyValue::Ascii(s) => {
270 let ascii_with_pipe = format!("{}|", s);
271 let count =
272 checked_u16_len(key.id, GEO_ASCII_PARAMS_TAG, ascii_with_pipe.len())?;
273 let offset =
274 checked_u16_offset(key.id, GEO_ASCII_PARAMS_TAG, ascii_params.len())?;
275 directory.push(GEO_ASCII_PARAMS_TAG); directory.push(count);
277 directory.push(offset);
278 ascii_params.push_str(&ascii_with_pipe);
279 }
280 }
281 }
282
283 Ok((directory, double_params, ascii_params))
284 }
285}
286
287fn checked_u16_len(key_id: u16, tag: u16, count: usize) -> Result<u16, GeoKeySerializeError> {
288 u16::try_from(count).map_err(|_| GeoKeySerializeError::ValueCountTooLarge {
289 key_id,
290 tag,
291 count,
292 })
293}
294
295fn checked_u16_offset(key_id: u16, tag: u16, offset: usize) -> Result<u16, GeoKeySerializeError> {
296 u16::try_from(offset).map_err(|_| GeoKeySerializeError::ParameterOffsetTooLarge {
297 key_id,
298 tag,
299 offset,
300 })
301}
302
303impl Default for GeoKeyDirectory {
304 fn default() -> Self {
305 Self::new()
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn parse_roundtrip() {
315 let mut dir = GeoKeyDirectory::new();
316 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
317 dir.set(GEOGRAPHIC_TYPE, GeoKeyValue::Short(4326));
318 dir.set(GEOG_CITATION, GeoKeyValue::Ascii("WGS 84".into()));
319
320 let (shorts, doubles, ascii) = dir.serialize().unwrap();
321 let parsed = GeoKeyDirectory::parse(&shorts, &doubles, &ascii).unwrap();
322
323 assert_eq!(parsed.get_short(GT_MODEL_TYPE), Some(2));
324 assert_eq!(parsed.get_short(GEOGRAPHIC_TYPE), Some(4326));
325 assert_eq!(parsed.get_ascii(GEOG_CITATION), Some("WGS 84"));
326 }
327
328 #[test]
329 fn set_replaces_existing() {
330 let mut dir = GeoKeyDirectory::new();
331 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
332 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
333 assert_eq!(dir.get_short(GT_MODEL_TYPE), Some(2));
334 assert_eq!(dir.keys.len(), 1);
335 }
336
337 #[test]
338 fn remove_key() {
339 let mut dir = GeoKeyDirectory::new();
340 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
341 dir.remove(GT_MODEL_TYPE);
342 assert!(dir.get(GT_MODEL_TYPE).is_none());
343 }
344
345 #[test]
346 fn parse_skips_invalid_ascii_subslice_without_panicking() {
347 let directory = [
348 1u16,
349 1,
350 0,
351 1, GEOG_CITATION,
353 34737,
354 1,
355 1, ];
357 let ascii = String::from_utf8_lossy(&[0xff, b'|']).into_owned();
358
359 let parsed = GeoKeyDirectory::parse(&directory, &[], &ascii).unwrap();
360 assert!(parsed.get_ascii(GEOG_CITATION).is_none());
361 }
362
363 #[test]
364 fn serialize_rejects_too_many_keys() {
365 let mut dir = GeoKeyDirectory::new();
366 dir.keys = (0..=u16::MAX as usize)
367 .map(|index| GeoKey {
368 id: index as u16,
369 value: GeoKeyValue::Short(1),
370 })
371 .collect();
372
373 let err = dir.serialize().unwrap_err();
374 assert_eq!(
375 err,
376 GeoKeySerializeError::TooManyKeys {
377 count: u16::MAX as usize + 1
378 }
379 );
380 }
381
382 #[test]
383 fn serialize_rejects_oversized_double_value_count() {
384 let mut dir = GeoKeyDirectory::new();
385 dir.set(
386 GT_CITATION,
387 GeoKeyValue::Double(vec![1.0; u16::MAX as usize + 1]),
388 );
389
390 let err = dir.serialize().unwrap_err();
391 assert_eq!(
392 err,
393 GeoKeySerializeError::ValueCountTooLarge {
394 key_id: GT_CITATION,
395 tag: GEO_DOUBLE_PARAMS_TAG,
396 count: u16::MAX as usize + 1
397 }
398 );
399 }
400
401 #[test]
402 fn serialize_rejects_oversized_ascii_parameter_offset() {
403 let mut dir = GeoKeyDirectory::new();
404 dir.set(
405 GEOG_CITATION,
406 GeoKeyValue::Ascii("a".repeat(u16::MAX as usize - 1)),
407 );
408 dir.set(PROJ_CITATION, GeoKeyValue::Ascii("b".to_string()));
409 dir.set(VERTICAL_CITATION, GeoKeyValue::Ascii("c".to_string()));
410
411 let err = dir.serialize().unwrap_err();
412 assert_eq!(
413 err,
414 GeoKeySerializeError::ParameterOffsetTooLarge {
415 key_id: VERTICAL_CITATION,
416 tag: GEO_ASCII_PARAMS_TAG,
417 offset: u16::MAX as usize + 2
418 }
419 );
420 }
421}