1use crate::operation::{AreaOfUse, GridId, GridInterpolation, GridShiftDirection};
2use smallvec::SmallVec;
3use std::collections::HashMap;
4use std::f64::consts::PI;
5use std::io::Read;
6use std::path::{Component, Path, PathBuf};
7use std::sync::{Arc, Condvar, Mutex, OnceLock};
8use thiserror::Error;
9
10const NTV2_HEADER_LEN: usize = 11 * 16;
11const NTV2_RECORD_LEN: usize = 4 * 4;
12const MAX_NTV2_SUBFILES: usize = 4_096;
13const MAX_NTV2_CELLS_PER_SUBGRID: usize = 16_777_216;
14const MAX_NTV2_TOTAL_CELLS: usize = 16_777_216;
15const MAX_NTV2_TOTAL_DATA_BYTES: usize = MAX_NTV2_TOTAL_CELLS * NTV2_RECORD_LEN;
16const MAX_NTV2_GRID_BYTES: usize =
17 MAX_NTV2_TOTAL_DATA_BYTES + (MAX_NTV2_SUBFILES + 1) * NTV2_HEADER_LEN;
18const GTX_HEADER_LEN: usize = 40;
19const GTX_RECORD_LEN: usize = 4;
20const MAX_GTX_CELLS: usize = 16_777_216;
21const MAX_GTX_GRID_BYTES: usize = GTX_HEADER_LEN + MAX_GTX_CELLS * GTX_RECORD_LEN;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum GridFormat {
25 Ntv2,
27 Gtx,
29 Unsupported,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub struct GridDefinition {
34 pub id: GridId,
35 pub name: String,
36 pub format: GridFormat,
37 pub interpolation: GridInterpolation,
38 pub area_of_use: Option<AreaOfUse>,
39 pub resource_names: SmallVec<[String; 2]>,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub struct GridSample {
44 pub lon_shift_radians: f64,
45 pub lat_shift_radians: f64,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq)]
49pub struct VerticalGridSample {
50 pub offset_meters: f64,
52}
53
54#[derive(Debug, Error, Clone)]
55pub enum GridError {
56 #[error("grid not found: {0}")]
57 NotFound(String),
58 #[error("grid resource unavailable: {0}")]
59 Unavailable(String),
60 #[error("grid parse error: {0}")]
61 Parse(String),
62 #[error("grid point outside coverage: {0}")]
63 OutsideCoverage(String),
64 #[error("unsupported grid format: {0}")]
65 UnsupportedFormat(String),
66}
67
68pub trait GridProvider: Send + Sync {
69 fn definition(
70 &self,
71 grid: &GridDefinition,
72 ) -> std::result::Result<Option<GridDefinition>, GridError>;
73 fn load(&self, grid: &GridDefinition) -> std::result::Result<Option<GridHandle>, GridError>;
74}
75
76#[derive(Clone)]
77pub struct GridHandle {
78 definition: GridDefinition,
79 data: Arc<CachedGridData>,
80}
81
82impl GridHandle {
83 pub fn from_bytes(
88 definition: GridDefinition,
89 bytes: &[u8],
90 ) -> std::result::Result<Self, GridError> {
91 Ok(Self {
92 data: Arc::new(parse_cached_grid_data(
93 definition.format,
94 &definition.name,
95 bytes,
96 )?),
97 definition,
98 })
99 }
100
101 pub fn definition(&self) -> &GridDefinition {
102 &self.definition
103 }
104
105 pub fn checksum(&self) -> &str {
106 &self.data.checksum
107 }
108
109 pub fn sample(
110 &self,
111 lon_radians: f64,
112 lat_radians: f64,
113 ) -> std::result::Result<GridSample, GridError> {
114 match &self.data.data {
115 GridData::Ntv2(set) => set.sample(lon_radians, lat_radians),
116 GridData::Gtx(_) => Err(GridError::UnsupportedFormat(format!(
117 "{} is a vertical grid",
118 self.definition.name
119 ))),
120 }
121 }
122
123 pub fn sample_vertical_offset_meters(
124 &self,
125 lon_radians: f64,
126 lat_radians: f64,
127 ) -> std::result::Result<VerticalGridSample, GridError> {
128 match &self.data.data {
129 GridData::Gtx(grid) => grid.sample(lon_radians, lat_radians),
130 GridData::Ntv2(_) => Err(GridError::UnsupportedFormat(format!(
131 "{} is a horizontal grid",
132 self.definition.name
133 ))),
134 }
135 }
136
137 pub fn apply(
138 &self,
139 lon_radians: f64,
140 lat_radians: f64,
141 direction: GridShiftDirection,
142 ) -> std::result::Result<(f64, f64), GridError> {
143 match &self.data.data {
144 GridData::Ntv2(set) => set.apply(lon_radians, lat_radians, direction),
145 GridData::Gtx(_) => Err(GridError::UnsupportedFormat(format!(
146 "{} is a vertical grid",
147 self.definition.name
148 ))),
149 }
150 }
151}
152
153pub(crate) struct GridRuntime {
154 providers: Vec<Arc<dyn GridProvider>>,
155 definition_cache: Mutex<HashMap<String, GridDefinition>>,
156 handle_cache: Mutex<HashMap<String, GridHandle>>,
157}
158
159impl GridRuntime {
160 pub(crate) fn new(app_provider: Option<Arc<dyn GridProvider>>) -> Self {
161 let mut providers: Vec<Arc<dyn GridProvider>> = Vec::with_capacity(2);
162 if let Some(provider) = app_provider {
163 providers.push(provider);
164 }
165 providers.push(Arc::new(EmbeddedGridProvider));
166 Self {
167 providers,
168 definition_cache: Mutex::new(HashMap::new()),
169 handle_cache: Mutex::new(HashMap::new()),
170 }
171 }
172
173 pub(crate) fn resolve_definition(
174 &self,
175 grid: &GridDefinition,
176 ) -> std::result::Result<GridDefinition, GridError> {
177 let cache_key = grid_runtime_cache_key(grid);
178 if let Some(cached) = self
179 .definition_cache
180 .lock()
181 .expect("grid definition cache poisoned")
182 .get(&cache_key)
183 .cloned()
184 {
185 return Ok(cached);
186 }
187
188 for provider in &self.providers {
189 if let Some(definition) = provider.definition(grid)? {
190 self.definition_cache
191 .lock()
192 .expect("grid definition cache poisoned")
193 .insert(cache_key, definition.clone());
194 return Ok(definition);
195 }
196 }
197
198 Err(GridError::Unavailable(grid.name.clone()))
199 }
200
201 pub(crate) fn resolve_handle(
202 &self,
203 grid: &GridDefinition,
204 ) -> std::result::Result<GridHandle, GridError> {
205 let cache_key = grid_runtime_cache_key(grid);
206 if let Some(cached) = self
207 .handle_cache
208 .lock()
209 .expect("grid handle cache poisoned")
210 .get(&cache_key)
211 .cloned()
212 {
213 return Ok(cached);
214 }
215
216 let definition = self.resolve_definition(grid)?;
217 for provider in &self.providers {
218 if let Some(handle) = provider.load(&definition)? {
219 self.handle_cache
220 .lock()
221 .expect("grid handle cache poisoned")
222 .insert(cache_key, handle.clone());
223 return Ok(handle);
224 }
225 }
226
227 Err(GridError::Unavailable(definition.name))
228 }
229}
230
231fn grid_runtime_cache_key(grid: &GridDefinition) -> String {
232 let mut key = format!("{}|{:?}", grid.id.0, grid.format);
233 for resource in &grid.resource_names {
234 key.push('|');
235 key.push_str(resource);
236 }
237 key
238}
239
240#[derive(Default)]
241pub struct EmbeddedGridProvider;
242
243impl GridProvider for EmbeddedGridProvider {
244 fn definition(
245 &self,
246 grid: &GridDefinition,
247 ) -> std::result::Result<Option<GridDefinition>, GridError> {
248 if embedded_grid_resource(&grid.resource_names).is_some() {
249 return Ok(Some(grid.clone()));
250 }
251 Ok(None)
252 }
253
254 fn load(&self, grid: &GridDefinition) -> std::result::Result<Option<GridHandle>, GridError> {
255 let Some((resource_name, bytes)) = embedded_grid_resource(&grid.resource_names) else {
256 return Ok(None);
257 };
258
259 let key = GridDataCacheKey::new(grid.format, resource_name);
260 let data = cached_grid_data(embedded_grid_data_cache(), key, || {
261 parse_cached_grid_data(grid.format, &grid.name, bytes)
262 })?;
263
264 Ok(Some(GridHandle {
265 definition: grid.clone(),
266 data,
267 }))
268 }
269}
270
271pub struct FilesystemGridProvider {
272 roots: Mutex<Vec<FilesystemGridRoot>>,
273 path_cache: Mutex<HashMap<String, PathBuf>>,
274 data_cache: GridDataCache,
275 #[cfg(test)]
276 locate_searches: std::sync::atomic::AtomicUsize,
277}
278
279enum FilesystemGridRoot {
280 Canonical(PathBuf),
281 Unresolved(PathBuf),
284}
285
286impl FilesystemGridProvider {
287 pub fn new<I>(roots: I) -> Self
288 where
289 I: IntoIterator<Item = PathBuf>,
290 {
291 Self {
292 roots: Mutex::new(
293 roots
294 .into_iter()
295 .map(|root| match root.canonicalize() {
296 Ok(canonical_root) => FilesystemGridRoot::Canonical(canonical_root),
297 Err(_) => FilesystemGridRoot::Unresolved(root),
298 })
299 .collect(),
300 ),
301 path_cache: Mutex::new(HashMap::new()),
302 data_cache: Mutex::new(HashMap::new()),
303 #[cfg(test)]
304 locate_searches: std::sync::atomic::AtomicUsize::new(0),
305 }
306 }
307
308 fn locate(&self, grid: &GridDefinition) -> Option<PathBuf> {
309 let cache_key = grid_runtime_cache_key(grid);
310 if let Some(path) = self
311 .path_cache
312 .lock()
313 .expect("filesystem grid path cache poisoned")
314 .get(&cache_key)
315 .cloned()
316 {
317 return Some(path);
318 }
319
320 let path = self.locate_uncached(grid)?;
321 self.path_cache
322 .lock()
323 .expect("filesystem grid path cache poisoned")
324 .insert(cache_key, path.clone());
325 Some(path)
326 }
327
328 fn locate_uncached(&self, grid: &GridDefinition) -> Option<PathBuf> {
329 #[cfg(test)]
330 self.locate_searches
331 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
332
333 let safe_resource_names = grid
334 .resource_names
335 .iter()
336 .filter(|name| is_safe_grid_resource_name(name))
337 .collect::<Vec<_>>();
338 if safe_resource_names.is_empty() {
339 return None;
340 }
341
342 for root in self.canonical_roots_for_lookup() {
343 for name in &safe_resource_names {
344 let candidate = root.join(name);
345 let Ok(canonical_candidate) = candidate.canonicalize() else {
346 continue;
347 };
348 if canonical_candidate.starts_with(&root) && canonical_candidate.is_file() {
349 return Some(canonical_candidate);
350 }
351 }
352 }
353 None
354 }
355
356 fn canonical_roots_for_lookup(&self) -> Vec<PathBuf> {
357 let mut roots = self.roots.lock().expect("filesystem grid roots poisoned");
358 let mut canonical_roots = Vec::with_capacity(roots.len());
359 for root in roots.iter_mut() {
360 match root {
361 FilesystemGridRoot::Canonical(canonical_root) => {
362 canonical_roots.push(canonical_root.clone());
363 }
364 FilesystemGridRoot::Unresolved(unresolved_root) => {
365 let Ok(canonical_root) = unresolved_root.canonicalize() else {
366 continue;
367 };
368 *root = FilesystemGridRoot::Canonical(canonical_root.clone());
369 canonical_roots.push(canonical_root);
370 }
371 }
372 }
373 canonical_roots
374 }
375}
376
377impl GridProvider for FilesystemGridProvider {
378 fn definition(
379 &self,
380 grid: &GridDefinition,
381 ) -> std::result::Result<Option<GridDefinition>, GridError> {
382 if self.locate(grid).is_some() {
383 return Ok(Some(grid.clone()));
384 }
385 Ok(None)
386 }
387
388 fn load(&self, grid: &GridDefinition) -> std::result::Result<Option<GridHandle>, GridError> {
389 let Some(path) = self.locate(grid) else {
390 return Ok(None);
391 };
392
393 let key = GridDataCacheKey::new(grid.format, path.to_string_lossy());
394 let data = cached_grid_data(&self.data_cache, key, || {
395 let bytes = read_grid_resource_bytes(&path, grid.format)?;
396 parse_cached_grid_data(grid.format, &grid.name, &bytes)
397 })?;
398
399 Ok(Some(GridHandle {
400 definition: grid.clone(),
401 data,
402 }))
403 }
404}
405
406fn is_safe_grid_resource_name(name: &str) -> bool {
407 let path = Path::new(name);
408 if path.as_os_str().is_empty() {
409 return false;
410 }
411 path.components()
412 .all(|component| matches!(component, Component::Normal(_)))
413}
414
415fn read_grid_resource_bytes(
416 path: &Path,
417 format: GridFormat,
418) -> std::result::Result<Vec<u8>, GridError> {
419 if let Some(max_bytes) = max_grid_resource_bytes(format) {
420 return read_bounded_grid_resource_bytes(path, format, max_bytes);
421 }
422
423 std::fs::read(path).map_err(|err| GridError::Unavailable(format!("{}: {err}", path.display())))
424}
425
426fn read_bounded_grid_resource_bytes(
427 path: &Path,
428 format: GridFormat,
429 max_bytes: usize,
430) -> std::result::Result<Vec<u8>, GridError> {
431 let file = std::fs::File::open(path)
432 .map_err(|err| GridError::Unavailable(format!("{}: {err}", path.display())))?;
433 let read_limit = u64::try_from(max_bytes)
434 .unwrap_or(u64::MAX)
435 .saturating_add(1);
436 let mut reader = file.take(read_limit);
437 let mut bytes = Vec::with_capacity(max_bytes.min(8192));
438 reader
439 .read_to_end(&mut bytes)
440 .map_err(|err| GridError::Unavailable(format!("{}: {err}", path.display())))?;
441
442 if bytes.len() > max_bytes {
443 return Err(GridError::Parse(format!(
444 "{} exceeds maximum {format:?} grid size of {max_bytes} bytes",
445 path.display()
446 )));
447 }
448
449 Ok(bytes)
450}
451
452fn validate_grid_resource_size(
453 resource: impl std::fmt::Display,
454 format: GridFormat,
455 len: u64,
456) -> std::result::Result<(), GridError> {
457 if let Some(max_bytes) = max_grid_resource_bytes(format) {
458 let max_bytes_u64 = u64::try_from(max_bytes).unwrap_or(u64::MAX);
459 if len > max_bytes_u64 {
460 return Err(GridError::Parse(format!(
461 "{resource} exceeds maximum {format:?} grid size of {max_bytes} bytes"
462 )));
463 }
464 }
465 Ok(())
466}
467
468fn max_grid_resource_bytes(format: GridFormat) -> Option<usize> {
469 match format {
470 GridFormat::Ntv2 => Some(MAX_NTV2_GRID_BYTES),
471 GridFormat::Gtx => Some(MAX_GTX_GRID_BYTES),
472 GridFormat::Unsupported => None,
473 }
474}
475
476enum GridData {
477 Ntv2(Ntv2GridSet),
478 Gtx(GtxGrid),
479}
480
481struct CachedGridData {
482 data: GridData,
483 checksum: String,
484}
485
486type GridDataCache = Mutex<HashMap<GridDataCacheKey, Arc<GridDataCacheSlot>>>;
487
488struct GridDataCacheSlot {
489 state: Mutex<GridDataCacheState>,
490 ready: Condvar,
491}
492
493enum GridDataCacheState {
494 Loading,
495 Ready(Arc<CachedGridData>),
496 Failed(GridError),
497}
498
499impl GridDataCacheSlot {
500 fn loading() -> Self {
501 Self {
502 state: Mutex::new(GridDataCacheState::Loading),
503 ready: Condvar::new(),
504 }
505 }
506}
507
508#[derive(Debug, Clone, PartialEq, Eq, Hash)]
509struct GridDataCacheKey {
510 format: GridFormat,
511 resource: String,
512}
513
514impl GridDataCacheKey {
515 fn new(format: GridFormat, resource: impl AsRef<str>) -> Self {
516 Self {
517 format,
518 resource: resource.as_ref().to_string(),
519 }
520 }
521}
522
523fn embedded_grid_data_cache() -> &'static GridDataCache {
524 static CACHE: OnceLock<GridDataCache> = OnceLock::new();
525 CACHE.get_or_init(|| Mutex::new(HashMap::new()))
526}
527
528fn cached_grid_data(
529 cache: &GridDataCache,
530 key: GridDataCacheKey,
531 parse: impl FnOnce() -> std::result::Result<CachedGridData, GridError>,
532) -> std::result::Result<Arc<CachedGridData>, GridError> {
533 let (slot, should_load) = {
534 let mut cache = cache.lock().expect("grid data cache poisoned");
535 if let Some(slot) = cache.get(&key) {
536 (Arc::clone(slot), false)
537 } else {
538 let slot = Arc::new(GridDataCacheSlot::loading());
539 cache.insert(key.clone(), Arc::clone(&slot));
540 (slot, true)
541 }
542 };
543
544 if should_load {
545 let result = parse().map(Arc::new);
546 if result.is_err() {
547 let mut cache = cache.lock().expect("grid data cache poisoned");
548 let should_remove = cache
549 .get(&key)
550 .map(|cached_slot| Arc::ptr_eq(cached_slot, &slot))
551 .unwrap_or(false);
552 if should_remove {
553 cache.remove(&key);
554 }
555 }
556
557 let mut state = slot.state.lock().expect("grid data cache slot poisoned");
558 match &result {
559 Ok(data) => *state = GridDataCacheState::Ready(Arc::clone(data)),
560 Err(error) => *state = GridDataCacheState::Failed(error.clone()),
561 }
562 slot.ready.notify_all();
563 return result;
564 }
565
566 let mut state = slot.state.lock().expect("grid data cache slot poisoned");
567 loop {
568 match &*state {
569 GridDataCacheState::Ready(data) => return Ok(Arc::clone(data)),
570 GridDataCacheState::Failed(error) => return Err(error.clone()),
571 GridDataCacheState::Loading => {
572 state = slot
573 .ready
574 .wait(state)
575 .expect("grid data cache slot poisoned");
576 }
577 }
578 }
579}
580
581fn parse_grid_data(
582 format: GridFormat,
583 name: &str,
584 bytes: &[u8],
585) -> std::result::Result<GridData, GridError> {
586 validate_grid_resource_size(name, format, u64::try_from(bytes.len()).unwrap_or(u64::MAX))?;
587
588 match format {
589 GridFormat::Ntv2 => Ok(GridData::Ntv2(Ntv2GridSet::parse(bytes)?)),
590 GridFormat::Gtx => Ok(GridData::Gtx(GtxGrid::parse(bytes)?)),
591 GridFormat::Unsupported => Err(GridError::UnsupportedFormat(name.into())),
592 }
593}
594
595fn parse_cached_grid_data(
596 format: GridFormat,
597 name: &str,
598 bytes: &[u8],
599) -> std::result::Result<CachedGridData, GridError> {
600 Ok(CachedGridData {
601 data: parse_grid_data(format, name, bytes)?,
602 checksum: sha256_hex(bytes),
603 })
604}
605
606fn sha256_hex(bytes: &[u8]) -> String {
607 const H0: [u32; 8] = [
608 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
609 0x5be0cd19,
610 ];
611 const K: [u32; 64] = [
612 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
613 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
614 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
615 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
616 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
617 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
618 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
619 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
620 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
621 0xc67178f2,
622 ];
623
624 let bit_len = (bytes.len() as u64).wrapping_mul(8);
625 let mut padded = Vec::with_capacity((bytes.len() + 72).div_ceil(64) * 64);
626 padded.extend_from_slice(bytes);
627 padded.push(0x80);
628 while (padded.len() % 64) != 56 {
629 padded.push(0);
630 }
631 padded.extend_from_slice(&bit_len.to_be_bytes());
632
633 let mut h = H0;
634 let mut w = [0u32; 64];
635 for chunk in padded.chunks_exact(64) {
636 for (i, word) in w.iter_mut().take(16).enumerate() {
637 *word = u32::from_be_bytes(
638 chunk[i * 4..i * 4 + 4]
639 .try_into()
640 .expect("slice length checked"),
641 );
642 }
643 for i in 16..64 {
644 let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
645 let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
646 w[i] = w[i - 16]
647 .wrapping_add(s0)
648 .wrapping_add(w[i - 7])
649 .wrapping_add(s1);
650 }
651
652 let mut a = h[0];
653 let mut b = h[1];
654 let mut c = h[2];
655 let mut d = h[3];
656 let mut e = h[4];
657 let mut f = h[5];
658 let mut g = h[6];
659 let mut hh = h[7];
660
661 for i in 0..64 {
662 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
663 let ch = (e & f) ^ ((!e) & g);
664 let temp1 = hh
665 .wrapping_add(s1)
666 .wrapping_add(ch)
667 .wrapping_add(K[i])
668 .wrapping_add(w[i]);
669 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
670 let maj = (a & b) ^ (a & c) ^ (b & c);
671 let temp2 = s0.wrapping_add(maj);
672
673 hh = g;
674 g = f;
675 f = e;
676 e = d.wrapping_add(temp1);
677 d = c;
678 c = b;
679 b = a;
680 a = temp1.wrapping_add(temp2);
681 }
682
683 h[0] = h[0].wrapping_add(a);
684 h[1] = h[1].wrapping_add(b);
685 h[2] = h[2].wrapping_add(c);
686 h[3] = h[3].wrapping_add(d);
687 h[4] = h[4].wrapping_add(e);
688 h[5] = h[5].wrapping_add(f);
689 h[6] = h[6].wrapping_add(g);
690 h[7] = h[7].wrapping_add(hh);
691 }
692
693 let mut out = String::with_capacity(71);
694 out.push_str("sha256:");
695 for word in h {
696 use std::fmt::Write as _;
697 write!(&mut out, "{word:08x}").expect("writing to string cannot fail");
698 }
699 out
700}
701
702fn embedded_grid_resource(names: &[String]) -> Option<(&'static str, &'static [u8])> {
703 for name in names {
704 if name.eq_ignore_ascii_case("ntv2_0.gsb") {
705 return Some(("ntv2_0.gsb", include_bytes!("../data/grids/ntv2_0.gsb")));
706 }
707 }
708 None
709}
710
711#[derive(Clone)]
712struct Ntv2GridSet {
713 grids: Vec<Ntv2Grid>,
714 roots: Vec<usize>,
715}
716
717impl Ntv2GridSet {
718 fn parse(bytes: &[u8]) -> std::result::Result<Self, GridError> {
719 if bytes.len() < NTV2_HEADER_LEN {
720 return Err(GridError::Parse("NTv2 file too small".into()));
721 }
722 if bytes.len() > MAX_NTV2_GRID_BYTES {
723 return Err(GridError::Parse(format!(
724 "NTv2 grid exceeds maximum size of {MAX_NTV2_GRID_BYTES} bytes"
725 )));
726 }
727
728 let endian = if u32::from_le_bytes(bytes[8..12].try_into().expect("slice length checked"))
729 == 11
730 {
731 Endian::Little
732 } else if u32::from_be_bytes(bytes[8..12].try_into().expect("slice length checked")) == 11 {
733 Endian::Big
734 } else {
735 return Err(GridError::Parse(
736 "invalid NTv2 header endianness marker".into(),
737 ));
738 };
739
740 if &bytes[56..63] != b"SECONDS" {
741 return Err(GridError::Parse(
742 "only NTv2 GS_TYPE=SECONDS is supported".into(),
743 ));
744 }
745
746 let num_subfiles = read_u32(bytes, 40, endian)? as usize;
747 if num_subfiles == 0 || num_subfiles > MAX_NTV2_SUBFILES {
748 return Err(GridError::Parse(format!(
749 "NTv2 subfile count {num_subfiles} exceeds limit {MAX_NTV2_SUBFILES}"
750 )));
751 }
752
753 let mut offset = NTV2_HEADER_LEN;
754 let mut grids = Vec::with_capacity(num_subfiles);
755 let mut name_to_index = HashMap::new();
756 let mut parent_links: Vec<Option<String>> = Vec::with_capacity(num_subfiles);
757 let mut total_cells = 0usize;
758 let mut total_data_bytes = 0usize;
759
760 for _ in 0..num_subfiles {
761 let header_end = offset
762 .checked_add(NTV2_HEADER_LEN)
763 .ok_or_else(|| GridError::Parse("NTv2 header offset overflow".into()))?;
764 let header = bytes
765 .get(offset..header_end)
766 .ok_or_else(|| GridError::Parse("truncated NTv2 subfile header".into()))?;
767 if &header[0..8] != b"SUB_NAME" {
768 return Err(GridError::Parse("invalid NTv2 subfile header tag".into()));
769 }
770
771 let name = parse_label(&header[8..16]);
772 let parent = parse_label(&header[24..32]);
773 let south = read_f64(header, 72, endian)? * PI / 180.0 / 3600.0;
774 let north = read_f64(header, 88, endian)? * PI / 180.0 / 3600.0;
775 let east = -read_f64(header, 104, endian)? * PI / 180.0 / 3600.0;
776 let west = -read_f64(header, 120, endian)? * PI / 180.0 / 3600.0;
777 let res_y = read_f64(header, 136, endian)? * PI / 180.0 / 3600.0;
778 let res_x = read_f64(header, 152, endian)? * PI / 180.0 / 3600.0;
779 let gs_count = read_u32(header, 168, endian)? as usize;
780
781 if !(west.is_finite()
782 && east.is_finite()
783 && south.is_finite()
784 && north.is_finite()
785 && res_x.is_finite()
786 && res_y.is_finite()
787 && west < east
788 && south < north
789 && res_x > 0.0
790 && res_y > 0.0)
791 {
792 return Err(GridError::Parse(format!(
793 "invalid NTv2 georeferencing for subgrid {name}"
794 )));
795 }
796
797 let width = ntv2_axis_cell_count(east - west, res_x, "longitude", &name)?;
798 let height = ntv2_axis_cell_count(north - south, res_y, "latitude", &name)?;
799 let derived_cells = width
800 .checked_mul(height)
801 .ok_or_else(|| GridError::Parse("NTv2 cell count overflow".into()))?;
802 if derived_cells > MAX_NTV2_CELLS_PER_SUBGRID {
803 return Err(GridError::Parse(format!(
804 "NTv2 subgrid {name} has {derived_cells} cells, exceeding limit {MAX_NTV2_CELLS_PER_SUBGRID}"
805 )));
806 }
807 if derived_cells != gs_count {
808 return Err(GridError::Parse(format!(
809 "NTv2 subgrid {name} cell count mismatch: expected {} got {gs_count}",
810 derived_cells
811 )));
812 }
813
814 total_cells = total_cells
815 .checked_add(gs_count)
816 .ok_or_else(|| GridError::Parse("NTv2 total cell count overflow".into()))?;
817 if total_cells > MAX_NTV2_TOTAL_CELLS {
818 return Err(GridError::Parse(format!(
819 "NTv2 total cell count {total_cells} exceeds limit {MAX_NTV2_TOTAL_CELLS}"
820 )));
821 }
822
823 let data_len = gs_count
824 .checked_mul(NTV2_RECORD_LEN)
825 .ok_or_else(|| GridError::Parse("NTv2 data size overflow".into()))?;
826 total_data_bytes = total_data_bytes
827 .checked_add(data_len)
828 .ok_or_else(|| GridError::Parse("NTv2 total data size overflow".into()))?;
829 if total_data_bytes > MAX_NTV2_TOTAL_DATA_BYTES {
830 return Err(GridError::Parse(format!(
831 "NTv2 data size {total_data_bytes} exceeds limit {MAX_NTV2_TOTAL_DATA_BYTES}"
832 )));
833 }
834 let data_end = header_end
835 .checked_add(data_len)
836 .ok_or_else(|| GridError::Parse("NTv2 data offset overflow".into()))?;
837 let data = bytes.get(header_end..data_end).ok_or_else(|| {
838 GridError::Parse(format!("truncated NTv2 data for subgrid {name}"))
839 })?;
840
841 let mut lat_shift = vec![0.0f64; gs_count];
842 let mut lon_shift = vec![0.0f64; gs_count];
843 for y in 0..height {
844 for x in 0..width {
845 let source_x = width - 1 - x;
846 let record_offset = (y * width + source_x) * NTV2_RECORD_LEN;
847 let lat = read_f32(data, record_offset, endian)? as f64 * PI / 180.0 / 3600.0;
848 let lon =
849 -(read_f32(data, record_offset + 4, endian)? as f64) * PI / 180.0 / 3600.0;
850 if !(lat.is_finite() && lon.is_finite()) {
851 return Err(GridError::Parse(format!(
852 "non-finite NTv2 shift value in subgrid {name}"
853 )));
854 }
855 let dest = y * width + x;
856 lat_shift[dest] = lat;
857 lon_shift[dest] = lon;
858 }
859 }
860
861 let index = grids.len();
862 name_to_index.insert(name.clone(), index);
863 parent_links.push(
864 if parent.eq_ignore_ascii_case("none") || parent.is_empty() {
865 None
866 } else {
867 Some(parent)
868 },
869 );
870 grids.push(Ntv2Grid {
871 name,
872 extent: GridExtent {
873 west,
874 south,
875 east,
876 north,
877 res_x,
878 res_y,
879 },
880 width,
881 height,
882 lat_shift,
883 lon_shift,
884 children: Vec::new(),
885 });
886 offset = data_end;
887 }
888
889 let mut roots = Vec::new();
890 for (idx, parent) in parent_links.into_iter().enumerate() {
891 if let Some(parent_name) = parent {
892 let Some(parent_idx) = name_to_index.get(&parent_name).copied() else {
893 return Err(GridError::Parse(format!(
894 "missing NTv2 parent subgrid {parent_name} for {}",
895 grids[idx].name
896 )));
897 };
898 grids[parent_idx].children.push(idx);
899 } else {
900 roots.push(idx);
901 }
902 }
903
904 Ok(Self { grids, roots })
905 }
906
907 fn sample(
908 &self,
909 lon_radians: f64,
910 lat_radians: f64,
911 ) -> std::result::Result<GridSample, GridError> {
912 let (grid_idx, local_lon, local_lat) = self.grid_at(lon_radians, lat_radians)?;
913 let (lon_shift, lat_shift) = interpolate(&self.grids[grid_idx], local_lon, local_lat)?;
914 Ok(GridSample {
915 lon_shift_radians: lon_shift,
916 lat_shift_radians: lat_shift,
917 })
918 }
919
920 fn apply(
921 &self,
922 lon_radians: f64,
923 lat_radians: f64,
924 direction: GridShiftDirection,
925 ) -> std::result::Result<(f64, f64), GridError> {
926 match direction {
927 GridShiftDirection::Forward => {
928 let shift = self.sample(lon_radians, lat_radians)?;
929 Ok((
930 lon_radians + shift.lon_shift_radians,
931 lat_radians + shift.lat_shift_radians,
932 ))
933 }
934 GridShiftDirection::Reverse => self.apply_inverse(lon_radians, lat_radians),
935 }
936 }
937
938 fn apply_inverse(
939 &self,
940 lon_radians: f64,
941 lat_radians: f64,
942 ) -> std::result::Result<(f64, f64), GridError> {
943 const MAX_ITERATIONS: usize = 10;
944 const TOLERANCE: f64 = 1e-12;
945
946 let mut estimate_lon = lon_radians;
947 let mut estimate_lat = lat_radians;
948
949 for _ in 0..MAX_ITERATIONS {
950 let shift = self.sample(estimate_lon, estimate_lat)?;
951 let next_lon = lon_radians - shift.lon_shift_radians;
952 let next_lat = lat_radians - shift.lat_shift_radians;
953 let diff_lon = next_lon - estimate_lon;
954 let diff_lat = next_lat - estimate_lat;
955 estimate_lon = next_lon;
956 estimate_lat = next_lat;
957 if diff_lon * diff_lon + diff_lat * diff_lat <= TOLERANCE * TOLERANCE {
958 return Ok((estimate_lon, estimate_lat));
959 }
960 }
961
962 Ok((estimate_lon, estimate_lat))
963 }
964
965 fn grid_at(
966 &self,
967 lon_radians: f64,
968 lat_radians: f64,
969 ) -> std::result::Result<(usize, f64, f64), GridError> {
970 for &root in &self.roots {
971 if self.grids[root].extent.contains(lon_radians, lat_radians) {
972 let idx = self.deepest_child(root, lon_radians, lat_radians);
973 let extent = &self.grids[idx].extent;
974 return Ok((idx, lon_radians - extent.west, lat_radians - extent.south));
975 }
976 }
977 Err(GridError::OutsideCoverage(format!(
978 "longitude {:.8} latitude {:.8}",
979 lon_radians.to_degrees(),
980 lat_radians.to_degrees()
981 )))
982 }
983
984 fn deepest_child(&self, index: usize, lon_radians: f64, lat_radians: f64) -> usize {
985 for &child in &self.grids[index].children {
986 if self.grids[child].extent.contains(lon_radians, lat_radians) {
987 return self.deepest_child(child, lon_radians, lat_radians);
988 }
989 }
990 index
991 }
992}
993
994fn ntv2_axis_cell_count(
995 span: f64,
996 resolution: f64,
997 axis: &str,
998 name: &str,
999) -> std::result::Result<usize, GridError> {
1000 let intervals = span / resolution;
1001 if !intervals.is_finite() || intervals < 0.0 {
1002 return Err(GridError::Parse(format!(
1003 "invalid NTv2 {axis} spacing for subgrid {name}"
1004 )));
1005 }
1006
1007 let rounded_intervals = (intervals + 0.5).floor();
1008 if !rounded_intervals.is_finite() || rounded_intervals > (MAX_NTV2_CELLS_PER_SUBGRID - 1) as f64
1009 {
1010 return Err(GridError::Parse(format!(
1011 "NTv2 subgrid {name} {axis} cell count exceeds limit {MAX_NTV2_CELLS_PER_SUBGRID}"
1012 )));
1013 }
1014
1015 let count = rounded_intervals as usize + 1;
1016 if count < 2 {
1017 return Err(GridError::Parse(format!(
1018 "NTv2 subgrid {name} has fewer than two {axis} cells"
1019 )));
1020 }
1021 Ok(count)
1022}
1023
1024#[derive(Clone)]
1025struct Ntv2Grid {
1026 name: String,
1027 extent: GridExtent,
1028 width: usize,
1029 height: usize,
1030 lat_shift: Vec<f64>,
1031 lon_shift: Vec<f64>,
1032 children: Vec<usize>,
1033}
1034
1035#[derive(Clone, Copy)]
1036struct GridExtent {
1037 west: f64,
1038 south: f64,
1039 east: f64,
1040 north: f64,
1041 res_x: f64,
1042 res_y: f64,
1043}
1044
1045impl GridExtent {
1046 fn contains(&self, lon_radians: f64, lat_radians: f64) -> bool {
1047 let epsilon = (self.res_x + self.res_y) * 1e-10;
1048 lon_radians >= self.west - epsilon
1049 && lon_radians <= self.east + epsilon
1050 && lat_radians >= self.south - epsilon
1051 && lat_radians <= self.north + epsilon
1052 }
1053}
1054
1055fn interpolate(
1056 grid: &Ntv2Grid,
1057 local_lon: f64,
1058 local_lat: f64,
1059) -> std::result::Result<(f64, f64), GridError> {
1060 let lam = local_lon / grid.extent.res_x;
1061 let phi = local_lat / grid.extent.res_y;
1062 let mut x = lam.floor() as isize;
1063 let mut y = phi.floor() as isize;
1064 let mut fx = lam - x as f64;
1065 let mut fy = phi - y as f64;
1066
1067 if x < 0 {
1068 if x == -1 && fx > 1.0 - 1e-9 {
1069 x = 0;
1070 fx = 0.0;
1071 } else {
1072 return Err(GridError::OutsideCoverage(grid.name.clone()));
1073 }
1074 }
1075 if y < 0 {
1076 if y == -1 && fy > 1.0 - 1e-9 {
1077 y = 0;
1078 fy = 0.0;
1079 } else {
1080 return Err(GridError::OutsideCoverage(grid.name.clone()));
1081 }
1082 }
1083 if x as usize + 1 >= grid.width {
1084 if x as usize + 1 == grid.width && fx < 1e-9 {
1085 x -= 1;
1086 fx = 1.0;
1087 } else {
1088 return Err(GridError::OutsideCoverage(grid.name.clone()));
1089 }
1090 }
1091 if y as usize + 1 >= grid.height {
1092 if y as usize + 1 == grid.height && fy < 1e-9 {
1093 y -= 1;
1094 fy = 1.0;
1095 } else {
1096 return Err(GridError::OutsideCoverage(grid.name.clone()));
1097 }
1098 }
1099
1100 let idx = |xx: usize, yy: usize| yy * grid.width + xx;
1101 let x0 = x as usize;
1102 let y0 = y as usize;
1103 let x1 = x0 + 1;
1104 let y1 = y0 + 1;
1105
1106 let m00 = (1.0 - fx) * (1.0 - fy);
1107 let m10 = fx * (1.0 - fy);
1108 let m01 = (1.0 - fx) * fy;
1109 let m11 = fx * fy;
1110
1111 let lon = m00 * grid.lon_shift[idx(x0, y0)]
1112 + m10 * grid.lon_shift[idx(x1, y0)]
1113 + m01 * grid.lon_shift[idx(x0, y1)]
1114 + m11 * grid.lon_shift[idx(x1, y1)];
1115 let lat = m00 * grid.lat_shift[idx(x0, y0)]
1116 + m10 * grid.lat_shift[idx(x1, y0)]
1117 + m01 * grid.lat_shift[idx(x0, y1)]
1118 + m11 * grid.lat_shift[idx(x1, y1)];
1119
1120 Ok((lon, lat))
1121}
1122
1123#[derive(Clone)]
1124struct GtxGrid {
1125 west_degrees: f64,
1126 south_degrees: f64,
1127 east_degrees: f64,
1128 north_degrees: f64,
1129 delta_lon_degrees: f64,
1130 delta_lat_degrees: f64,
1131 width: usize,
1132 height: usize,
1133 offsets_meters: Vec<f64>,
1134}
1135
1136impl GtxGrid {
1137 fn parse(bytes: &[u8]) -> std::result::Result<Self, GridError> {
1138 if bytes.len() < GTX_HEADER_LEN {
1139 return Err(GridError::Parse("GTX file too small".into()));
1140 }
1141 if bytes.len() > MAX_GTX_GRID_BYTES {
1142 return Err(GridError::Parse(format!(
1143 "GTX grid exceeds maximum size of {MAX_GTX_GRID_BYTES} bytes"
1144 )));
1145 }
1146
1147 let south_degrees = read_f64(bytes, 0, Endian::Big)?;
1148 let west_degrees = read_f64(bytes, 8, Endian::Big)?;
1149 let delta_lat_degrees = read_f64(bytes, 16, Endian::Big)?;
1150 let delta_lon_degrees = read_f64(bytes, 24, Endian::Big)?;
1151 let height_i32 = read_i32(bytes, 32, Endian::Big)?;
1152 let width_i32 = read_i32(bytes, 36, Endian::Big)?;
1153
1154 if !(west_degrees.is_finite()
1155 && south_degrees.is_finite()
1156 && delta_lon_degrees.is_finite()
1157 && delta_lat_degrees.is_finite()
1158 && delta_lon_degrees > 0.0
1159 && delta_lat_degrees > 0.0
1160 && width_i32 >= 2
1161 && height_i32 >= 2)
1162 {
1163 return Err(GridError::Parse("invalid GTX georeferencing".into()));
1164 }
1165 let height = height_i32 as usize;
1166 let width = width_i32 as usize;
1167
1168 let count = width
1169 .checked_mul(height)
1170 .ok_or_else(|| GridError::Parse("GTX data size overflow".into()))?;
1171 if count > MAX_GTX_CELLS {
1172 return Err(GridError::Parse(format!(
1173 "GTX cell count {count} exceeds limit {MAX_GTX_CELLS}"
1174 )));
1175 }
1176 let data_len = count
1177 .checked_mul(GTX_RECORD_LEN)
1178 .ok_or_else(|| GridError::Parse("GTX data size overflow".into()))?;
1179 let expected_len = GTX_HEADER_LEN
1180 .checked_add(data_len)
1181 .ok_or_else(|| GridError::Parse("GTX data size overflow".into()))?;
1182 if expected_len > MAX_GTX_GRID_BYTES {
1183 return Err(GridError::Parse(format!(
1184 "GTX data size {expected_len} exceeds limit {MAX_GTX_GRID_BYTES}"
1185 )));
1186 }
1187 if bytes.len() < expected_len {
1188 return Err(GridError::Parse("truncated GTX data".into()));
1189 }
1190
1191 let mut offsets_meters = Vec::with_capacity(count);
1192 for index in 0..count {
1193 let value =
1194 read_f32(bytes, GTX_HEADER_LEN + index * GTX_RECORD_LEN, Endian::Big)? as f64;
1195 if (value + 88.8888).abs() <= 1e-4 {
1196 offsets_meters.push(f64::NAN);
1197 } else {
1198 offsets_meters.push(value);
1199 }
1200 }
1201
1202 let east_degrees = west_degrees + delta_lon_degrees * (width - 1) as f64;
1203 let north_degrees = south_degrees + delta_lat_degrees * (height - 1) as f64;
1204
1205 Ok(Self {
1206 west_degrees,
1207 south_degrees,
1208 east_degrees,
1209 north_degrees,
1210 delta_lon_degrees,
1211 delta_lat_degrees,
1212 width,
1213 height,
1214 offsets_meters,
1215 })
1216 }
1217
1218 fn sample(
1219 &self,
1220 lon_radians: f64,
1221 lat_radians: f64,
1222 ) -> std::result::Result<VerticalGridSample, GridError> {
1223 let raw_lon_degrees = lon_radians.to_degrees();
1224 let lat_degrees = lat_radians.to_degrees();
1225
1226 if !(raw_lon_degrees.is_finite() && lat_degrees.is_finite()) {
1227 return Err(GridError::OutsideCoverage(format!(
1228 "non-finite longitude {:.8} latitude {:.8}",
1229 raw_lon_degrees, lat_degrees
1230 )));
1231 }
1232
1233 let lon_degrees = self.normalize_lon_degrees(raw_lon_degrees);
1234
1235 if !self.contains(lon_degrees, lat_degrees) {
1236 return Err(GridError::OutsideCoverage(format!(
1237 "longitude {:.8} latitude {:.8}",
1238 raw_lon_degrees, lat_degrees
1239 )));
1240 }
1241
1242 let lam = (lon_degrees - self.west_degrees) / self.delta_lon_degrees;
1243 let phi = (lat_degrees - self.south_degrees) / self.delta_lat_degrees;
1244 let mut x = lam.floor() as isize;
1245 let mut y = phi.floor() as isize;
1246 let mut fx = lam - x as f64;
1247 let mut fy = phi - y as f64;
1248
1249 if x < 0 {
1250 if x == -1 && fx > 1.0 - 1e-9 {
1251 x = 0;
1252 fx = 0.0;
1253 } else {
1254 return Err(GridError::OutsideCoverage("GTX negative grid index".into()));
1255 }
1256 }
1257 if y < 0 {
1258 if y == -1 && fy > 1.0 - 1e-9 {
1259 y = 0;
1260 fy = 0.0;
1261 } else {
1262 return Err(GridError::OutsideCoverage("GTX negative grid index".into()));
1263 }
1264 }
1265 if x as usize + 1 >= self.width {
1266 if x as usize + 1 == self.width && fx < 1e-9 {
1267 x -= 1;
1268 fx = 1.0;
1269 } else {
1270 return Err(GridError::OutsideCoverage("GTX longitude edge".into()));
1271 }
1272 }
1273 if y as usize + 1 >= self.height {
1274 if y as usize + 1 == self.height && fy < 1e-9 {
1275 y -= 1;
1276 fy = 1.0;
1277 } else {
1278 return Err(GridError::OutsideCoverage("GTX latitude edge".into()));
1279 }
1280 }
1281
1282 let x0 = x as usize;
1283 let y0 = y as usize;
1284 let x1 = x0 + 1;
1285 let y1 = y0 + 1;
1286 let idx = |xx: usize, yy: usize| yy * self.width + xx;
1287 let z00 = self.offsets_meters[idx(x0, y0)];
1288 let z10 = self.offsets_meters[idx(x1, y0)];
1289 let z01 = self.offsets_meters[idx(x0, y1)];
1290 let z11 = self.offsets_meters[idx(x1, y1)];
1291
1292 if !(z00.is_finite() && z10.is_finite() && z01.is_finite() && z11.is_finite()) {
1293 return Err(GridError::OutsideCoverage(
1294 "GTX interpolation touches a null cell".into(),
1295 ));
1296 }
1297
1298 let m00 = (1.0 - fx) * (1.0 - fy);
1299 let m10 = fx * (1.0 - fy);
1300 let m01 = (1.0 - fx) * fy;
1301 let m11 = fx * fy;
1302 Ok(VerticalGridSample {
1303 offset_meters: m00 * z00 + m10 * z10 + m01 * z01 + m11 * z11,
1304 })
1305 }
1306
1307 fn contains(&self, lon_degrees: f64, lat_degrees: f64) -> bool {
1308 let epsilon = (self.delta_lon_degrees + self.delta_lat_degrees) * 1e-10;
1309 lon_degrees >= self.west_degrees - epsilon
1310 && lon_degrees <= self.east_degrees + epsilon
1311 && lat_degrees >= self.south_degrees - epsilon
1312 && lat_degrees <= self.north_degrees + epsilon
1313 }
1314
1315 fn normalize_lon_degrees(&self, lon_degrees: f64) -> f64 {
1316 if self.contains(lon_degrees, self.south_degrees) {
1317 return lon_degrees;
1318 }
1319
1320 self.west_degrees + (lon_degrees - self.west_degrees).rem_euclid(360.0)
1321 }
1322}
1323
1324#[derive(Clone, Copy)]
1325enum Endian {
1326 Little,
1327 Big,
1328}
1329
1330fn parse_label(bytes: &[u8]) -> String {
1331 let end = bytes
1332 .iter()
1333 .position(|byte| *byte == 0)
1334 .unwrap_or(bytes.len());
1335 String::from_utf8_lossy(&bytes[..end]).trim().to_string()
1336}
1337
1338fn read_u32(bytes: &[u8], offset: usize, endian: Endian) -> std::result::Result<u32, GridError> {
1339 let end = offset
1340 .checked_add(4)
1341 .ok_or_else(|| GridError::Parse("integer offset overflow".into()))?;
1342 let slice = bytes
1343 .get(offset..end)
1344 .ok_or_else(|| GridError::Parse("truncated integer".into()))?;
1345 Ok(match endian {
1346 Endian::Little => u32::from_le_bytes(slice.try_into().expect("slice length checked")),
1347 Endian::Big => u32::from_be_bytes(slice.try_into().expect("slice length checked")),
1348 })
1349}
1350
1351fn read_i32(bytes: &[u8], offset: usize, endian: Endian) -> std::result::Result<i32, GridError> {
1352 let end = offset
1353 .checked_add(4)
1354 .ok_or_else(|| GridError::Parse("integer offset overflow".into()))?;
1355 let slice = bytes
1356 .get(offset..end)
1357 .ok_or_else(|| GridError::Parse("truncated integer".into()))?;
1358 Ok(match endian {
1359 Endian::Little => i32::from_le_bytes(slice.try_into().expect("slice length checked")),
1360 Endian::Big => i32::from_be_bytes(slice.try_into().expect("slice length checked")),
1361 })
1362}
1363
1364fn read_f64(bytes: &[u8], offset: usize, endian: Endian) -> std::result::Result<f64, GridError> {
1365 let end = offset
1366 .checked_add(8)
1367 .ok_or_else(|| GridError::Parse("float64 offset overflow".into()))?;
1368 let slice = bytes
1369 .get(offset..end)
1370 .ok_or_else(|| GridError::Parse("truncated float64".into()))?;
1371 Ok(match endian {
1372 Endian::Little => f64::from_le_bytes(slice.try_into().expect("slice length checked")),
1373 Endian::Big => f64::from_be_bytes(slice.try_into().expect("slice length checked")),
1374 })
1375}
1376
1377fn read_f32(bytes: &[u8], offset: usize, endian: Endian) -> std::result::Result<f32, GridError> {
1378 let end = offset
1379 .checked_add(4)
1380 .ok_or_else(|| GridError::Parse("float32 offset overflow".into()))?;
1381 let slice = bytes
1382 .get(offset..end)
1383 .ok_or_else(|| GridError::Parse("truncated float32".into()))?;
1384 Ok(match endian {
1385 Endian::Little => f32::from_le_bytes(slice.try_into().expect("slice length checked")),
1386 Endian::Big => f32::from_be_bytes(slice.try_into().expect("slice length checked")),
1387 })
1388}
1389
1390#[cfg(test)]
1391mod tests {
1392 use super::*;
1393 use proptest::prelude::*;
1394 use std::sync::atomic::{AtomicUsize, Ordering};
1395 use std::sync::Barrier;
1396 use std::time::Duration;
1397
1398 #[test]
1399 fn embedded_ntv2_grid_samples_known_point() {
1400 let provider = EmbeddedGridProvider;
1401 let definition = GridDefinition {
1402 id: GridId(1),
1403 name: "ntv2_0.gsb".into(),
1404 format: GridFormat::Ntv2,
1405 interpolation: GridInterpolation::Bilinear,
1406 area_of_use: None,
1407 resource_names: SmallVec::from_vec(vec!["ntv2_0.gsb".into()]),
1408 };
1409 let handle = provider.load(&definition).unwrap().expect("embedded grid");
1410 let (lon, lat) = handle
1411 .apply(
1412 (-80.5041667f64).to_radians(),
1413 44.5458333f64.to_radians(),
1414 GridShiftDirection::Forward,
1415 )
1416 .unwrap();
1417 assert!(
1418 (lon.to_degrees() - (-80.50401615833)).abs() < 1e-6,
1419 "lon={lon}"
1420 );
1421 assert!((lat.to_degrees() - 44.5458827236).abs() < 3e-6, "lat={lat}");
1422 }
1423
1424 #[test]
1425 fn embedded_provider_reuses_parsed_grid_data() {
1426 let provider = EmbeddedGridProvider;
1427 let definition = test_grid_definition();
1428
1429 let first = provider.load(&definition).unwrap().expect("embedded grid");
1430 let mut renamed = definition.clone();
1431 renamed.name = "renamed ntv2 grid".into();
1432 let second = provider.load(&renamed).unwrap().expect("embedded grid");
1433
1434 assert!(Arc::ptr_eq(&first.data, &second.data));
1435 assert_eq!(second.definition().name, "renamed ntv2 grid");
1436 }
1437
1438 #[test]
1439 fn grid_handle_reports_sha256_checksum() {
1440 let provider = EmbeddedGridProvider;
1441 let handle = provider
1442 .load(&test_grid_definition())
1443 .unwrap()
1444 .expect("embedded grid");
1445
1446 assert!(handle.checksum().starts_with("sha256:"));
1447 assert_eq!(handle.checksum().len(), 71);
1448 assert_eq!(
1449 sha256_hex(b"abc"),
1450 "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
1451 );
1452 }
1453
1454 struct SingleFlightTrackingProvider {
1455 data_cache: GridDataCache,
1456 parse_calls: Arc<AtomicUsize>,
1457 bytes: Vec<u8>,
1458 }
1459
1460 impl GridProvider for SingleFlightTrackingProvider {
1461 fn definition(
1462 &self,
1463 grid: &GridDefinition,
1464 ) -> std::result::Result<Option<GridDefinition>, GridError> {
1465 Ok(Some(grid.clone()))
1466 }
1467
1468 fn load(
1469 &self,
1470 grid: &GridDefinition,
1471 ) -> std::result::Result<Option<GridHandle>, GridError> {
1472 let key = GridDataCacheKey::new(grid.format, "single-flight-test-grid");
1473 let data = cached_grid_data(&self.data_cache, key, || {
1474 self.parse_calls.fetch_add(1, Ordering::SeqCst);
1475 std::thread::sleep(Duration::from_millis(25));
1476 parse_cached_grid_data(grid.format, &grid.name, &self.bytes)
1477 })?;
1478
1479 Ok(Some(GridHandle {
1480 definition: grid.clone(),
1481 data,
1482 }))
1483 }
1484 }
1485
1486 #[test]
1487 fn cached_grid_data_single_flights_concurrent_loads() {
1488 const THREADS: usize = 12;
1489
1490 let parse_calls = Arc::new(AtomicUsize::new(0));
1491 let provider = Arc::new(SingleFlightTrackingProvider {
1492 data_cache: Mutex::new(HashMap::new()),
1493 parse_calls: Arc::clone(&parse_calls),
1494 bytes: test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]),
1495 });
1496 let definition = GridDefinition {
1497 id: GridId(9_999),
1498 name: "single-flight-test.gtx".into(),
1499 format: GridFormat::Gtx,
1500 interpolation: GridInterpolation::Bilinear,
1501 area_of_use: None,
1502 resource_names: SmallVec::from_vec(vec!["single-flight-test.gtx".into()]),
1503 };
1504 let barrier = Arc::new(Barrier::new(THREADS));
1505
1506 let handles = std::thread::scope(|scope| {
1507 let mut joins = Vec::new();
1508 for _ in 0..THREADS {
1509 let provider = Arc::clone(&provider);
1510 let definition = definition.clone();
1511 let barrier = Arc::clone(&barrier);
1512 joins.push(scope.spawn(move || {
1513 barrier.wait();
1514 provider.load(&definition).unwrap().unwrap()
1515 }));
1516 }
1517
1518 joins
1519 .into_iter()
1520 .map(|join| join.join().unwrap())
1521 .collect::<Vec<_>>()
1522 });
1523
1524 assert_eq!(parse_calls.load(Ordering::SeqCst), 1);
1525 for handle in &handles[1..] {
1526 assert!(Arc::ptr_eq(&handles[0].data, &handle.data));
1527 assert_eq!(handles[0].checksum(), handle.checksum());
1528 }
1529 }
1530
1531 struct TrackingGridProvider {
1532 override_definition: bool,
1533 definition_calls: Arc<AtomicUsize>,
1534 load_calls: Arc<AtomicUsize>,
1535 }
1536
1537 impl GridProvider for TrackingGridProvider {
1538 fn definition(
1539 &self,
1540 grid: &GridDefinition,
1541 ) -> std::result::Result<Option<GridDefinition>, GridError> {
1542 self.definition_calls.fetch_add(1, Ordering::SeqCst);
1543 if self.override_definition {
1544 let mut overridden = grid.clone();
1545 overridden.name = "custom override".into();
1546 Ok(Some(overridden))
1547 } else {
1548 Ok(None)
1549 }
1550 }
1551
1552 fn load(
1553 &self,
1554 grid: &GridDefinition,
1555 ) -> std::result::Result<Option<GridHandle>, GridError> {
1556 self.load_calls.fetch_add(1, Ordering::SeqCst);
1557 EmbeddedGridProvider.load(grid)
1558 }
1559 }
1560
1561 fn test_grid_definition() -> GridDefinition {
1562 GridDefinition {
1563 id: GridId(1),
1564 name: "ntv2_0.gsb".into(),
1565 format: GridFormat::Ntv2,
1566 interpolation: GridInterpolation::Bilinear,
1567 area_of_use: None,
1568 resource_names: SmallVec::from_vec(vec!["ntv2_0.gsb".into()]),
1569 }
1570 }
1571
1572 fn write_ntv2_global_header(header: &mut [u8], num_subfiles: u32) {
1573 header[8..12].copy_from_slice(&11u32.to_le_bytes());
1574 header[40..44].copy_from_slice(&num_subfiles.to_le_bytes());
1575 header[56..63].copy_from_slice(b"SECONDS");
1576 }
1577
1578 fn write_ntv2_label(header: &mut [u8], offset: usize, value: &str) {
1579 header[offset..offset + 8].fill(b' ');
1580 let bytes = value.as_bytes();
1581 let len = bytes.len().min(8);
1582 header[offset..offset + len].copy_from_slice(&bytes[..len]);
1583 }
1584
1585 fn write_ntv2_f64(header: &mut [u8], offset: usize, value: f64) {
1586 header[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1587 }
1588
1589 fn write_ntv2_f64_bits(header: &mut [u8], offset: usize, bits: u64) {
1590 header[offset..offset + 8].copy_from_slice(&bits.to_le_bytes());
1591 }
1592
1593 fn write_ntv2_f32(bytes: &mut [u8], offset: usize, value: f32) {
1594 bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
1595 }
1596
1597 fn write_ntv2_u32(header: &mut [u8], offset: usize, value: u32) {
1598 header[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
1599 }
1600
1601 fn minimal_ntv2_bytes() -> Vec<u8> {
1602 let mut bytes = vec![0u8; NTV2_HEADER_LEN * 2 + 4 * NTV2_RECORD_LEN];
1603 write_ntv2_global_header(&mut bytes[..NTV2_HEADER_LEN], 1);
1604
1605 let header = &mut bytes[NTV2_HEADER_LEN..NTV2_HEADER_LEN * 2];
1606 header[0..8].copy_from_slice(b"SUB_NAME");
1607 write_ntv2_label(header, 8, "TEST");
1608 write_ntv2_label(header, 24, "NONE");
1609 write_ntv2_f64(header, 72, 0.0);
1610 write_ntv2_f64(header, 88, 3600.0);
1611 write_ntv2_f64(header, 104, 0.0);
1612 write_ntv2_f64(header, 120, 3600.0);
1613 write_ntv2_f64(header, 136, 3600.0);
1614 write_ntv2_f64(header, 152, 3600.0);
1615 write_ntv2_u32(header, 168, 4);
1616
1617 bytes
1618 }
1619
1620 fn grid_handle_parse_error(bytes: &[u8]) -> String {
1621 match GridHandle::from_bytes(test_grid_definition(), bytes) {
1622 Ok(_) => panic!("expected NTv2 parse failure"),
1623 Err(GridError::Parse(message)) => message,
1624 Err(error) => panic!("expected NTv2 parse error, got {error}"),
1625 }
1626 }
1627
1628 fn test_temp_grid_root(name: &str) -> PathBuf {
1629 static NEXT_ROOT: AtomicUsize = AtomicUsize::new(0);
1630
1631 let root = std::env::temp_dir().join(format!(
1632 "proj-core-{name}-{}-{}",
1633 std::process::id(),
1634 NEXT_ROOT.fetch_add(1, Ordering::SeqCst)
1635 ));
1636 let _ = std::fs::remove_dir_all(&root);
1637 std::fs::create_dir_all(&root).unwrap();
1638 root
1639 }
1640
1641 #[test]
1642 fn ntv2_rejects_oversized_resource_length_before_reading() {
1643 let message = match validate_grid_resource_size(
1644 "oversized.gsb",
1645 GridFormat::Ntv2,
1646 MAX_NTV2_GRID_BYTES as u64 + 1,
1647 ) {
1648 Ok(()) => panic!("expected NTv2 resource size failure"),
1649 Err(GridError::Parse(message)) => message,
1650 Err(error) => panic!("expected NTv2 parse error, got {error}"),
1651 };
1652
1653 assert!(message.contains("maximum Ntv2 grid size"), "{message}");
1654 }
1655
1656 #[test]
1657 fn grid_handle_rejects_excessive_ntv2_subfile_count_before_allocation() {
1658 let mut bytes = vec![0u8; NTV2_HEADER_LEN];
1659 write_ntv2_global_header(&mut bytes, u32::MAX);
1660
1661 let message = grid_handle_parse_error(&bytes);
1662
1663 assert!(message.contains("subfile count"), "{message}");
1664 }
1665
1666 #[test]
1667 fn ntv2_rejects_excessive_axis_count_before_cell_multiplication() {
1668 let mut bytes = minimal_ntv2_bytes();
1669 let header = &mut bytes[NTV2_HEADER_LEN..NTV2_HEADER_LEN * 2];
1670 write_ntv2_f64(header, 120, MAX_NTV2_CELLS_PER_SUBGRID as f64);
1671 write_ntv2_f64(header, 152, 1.0);
1672
1673 let message = grid_handle_parse_error(&bytes);
1674
1675 assert!(
1676 message.contains("longitude cell count exceeds limit"),
1677 "{message}"
1678 );
1679 }
1680
1681 #[test]
1682 fn ntv2_rejects_excessive_subgrid_cell_count_before_allocation() {
1683 let mut bytes = minimal_ntv2_bytes();
1684 let header = &mut bytes[NTV2_HEADER_LEN..NTV2_HEADER_LEN * 2];
1685 write_ntv2_f64(header, 88, 4096.0);
1686 write_ntv2_f64(header, 120, 4096.0);
1687 write_ntv2_f64(header, 136, 1.0);
1688 write_ntv2_f64(header, 152, 1.0);
1689 write_ntv2_u32(header, 168, 16_785_409);
1690
1691 let message = grid_handle_parse_error(&bytes);
1692
1693 assert!(message.contains("exceeding limit"), "{message}");
1694 }
1695
1696 #[test]
1697 fn ntv2_rejects_non_finite_shift_values() {
1698 let mut bytes = minimal_ntv2_bytes();
1699 write_ntv2_f32(&mut bytes, NTV2_HEADER_LEN * 2, f32::NAN);
1700
1701 let message = grid_handle_parse_error(&bytes);
1702
1703 assert!(message.contains("non-finite NTv2 shift value"), "{message}");
1704 }
1705
1706 proptest! {
1707 #![proptest_config(ProptestConfig::with_cases(256))]
1708
1709 #[test]
1710 fn ntv2_malformed_subfile_header_fuzz_does_not_panic(
1711 name in proptest::collection::vec(any::<u8>(), 8),
1712 parent in proptest::collection::vec(any::<u8>(), 8),
1713 south_bits in any::<u64>(),
1714 north_bits in any::<u64>(),
1715 east_bits in any::<u64>(),
1716 west_bits in any::<u64>(),
1717 res_y_bits in any::<u64>(),
1718 res_x_bits in any::<u64>(),
1719 gs_count in any::<u32>(),
1720 data in proptest::collection::vec(any::<u8>(), 0..512),
1721 ) {
1722 let mut bytes = vec![0u8; NTV2_HEADER_LEN * 2];
1723 write_ntv2_global_header(&mut bytes[..NTV2_HEADER_LEN], 1);
1724
1725 let header = &mut bytes[NTV2_HEADER_LEN..NTV2_HEADER_LEN * 2];
1726 header[0..8].copy_from_slice(b"SUB_NAME");
1727 header[8..16].copy_from_slice(&name);
1728 header[24..32].copy_from_slice(&parent);
1729 write_ntv2_f64_bits(header, 72, south_bits);
1730 write_ntv2_f64_bits(header, 88, north_bits);
1731 write_ntv2_f64_bits(header, 104, east_bits);
1732 write_ntv2_f64_bits(header, 120, west_bits);
1733 write_ntv2_f64_bits(header, 136, res_y_bits);
1734 write_ntv2_f64_bits(header, 152, res_x_bits);
1735 write_ntv2_u32(header, 168, gs_count);
1736 bytes.extend_from_slice(&data);
1737
1738 let _ = Ntv2GridSet::parse(&bytes);
1739 }
1740 }
1741
1742 #[test]
1743 fn filesystem_provider_rejects_unsafe_resource_names() {
1744 let root = test_temp_grid_root("unsafe-resource");
1745 std::fs::write(root.join("safe.gtx"), []).unwrap();
1746
1747 let provider = FilesystemGridProvider::new(vec![root.clone()]);
1748 let mut definition = test_grid_definition();
1749 definition.format = GridFormat::Gtx;
1750 definition.resource_names = SmallVec::from_vec(vec!["../safe.gtx".into()]);
1751 assert!(provider.definition(&definition).unwrap().is_none());
1752
1753 definition.resource_names =
1754 SmallVec::from_vec(vec![root.join("safe.gtx").to_string_lossy().into_owned()]);
1755 assert!(provider.definition(&definition).unwrap().is_none());
1756
1757 definition.resource_names = SmallVec::from_vec(vec!["safe.gtx".into()]);
1758 assert!(provider.definition(&definition).unwrap().is_some());
1759 }
1760
1761 #[test]
1762 fn filesystem_provider_loads_grid_from_canonical_root() {
1763 let root = test_temp_grid_root("canonical-root");
1764 std::fs::write(
1765 root.join("safe.gtx"),
1766 test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]),
1767 )
1768 .unwrap();
1769
1770 let provider = FilesystemGridProvider::new(vec![root]);
1771 let mut definition = test_grid_definition();
1772 definition.name = "safe.gtx".into();
1773 definition.format = GridFormat::Gtx;
1774 definition.resource_names = SmallVec::from_vec(vec!["safe.gtx".into()]);
1775
1776 assert!(provider.definition(&definition).unwrap().is_some());
1777 let handle = provider.load(&definition).unwrap().unwrap();
1778 let sample = handle
1779 .sample_vertical_offset_meters(20.5f64.to_radians(), 10.5f64.to_radians())
1780 .unwrap();
1781
1782 assert!((sample.offset_meters - 2.0).abs() < 1e-12);
1783 }
1784
1785 #[test]
1786 fn filesystem_provider_reuses_located_path_between_definition_and_load() {
1787 let root = test_temp_grid_root("path-cache");
1788 std::fs::write(
1789 root.join("cached.gtx"),
1790 test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]),
1791 )
1792 .unwrap();
1793
1794 let provider = FilesystemGridProvider::new(vec![root]);
1795 let mut definition = test_grid_definition();
1796 definition.name = "cached.gtx".into();
1797 definition.format = GridFormat::Gtx;
1798 definition.resource_names = SmallVec::from_vec(vec!["cached.gtx".into()]);
1799
1800 assert!(provider.definition(&definition).unwrap().is_some());
1801 assert_eq!(provider.locate_searches.load(Ordering::SeqCst), 1);
1802
1803 assert!(provider.load(&definition).unwrap().is_some());
1804 assert_eq!(provider.locate_searches.load(Ordering::SeqCst), 1);
1805 }
1806
1807 #[test]
1808 fn filesystem_grid_read_enforces_cap_on_bytes_read() {
1809 let root = test_temp_grid_root("bounded-read");
1810 let path = root.join("oversized.gtx");
1811 std::fs::write(&path, [0u8; 4]).unwrap();
1812
1813 let err = read_bounded_grid_resource_bytes(&path, GridFormat::Gtx, 3).unwrap_err();
1814
1815 let GridError::Parse(message) = err else {
1816 panic!("expected parse error");
1817 };
1818 assert!(
1819 message.contains("maximum Gtx grid size of 3 bytes"),
1820 "{message}"
1821 );
1822 }
1823
1824 fn test_gtx_bytes(values: &[f32]) -> Vec<u8> {
1825 let mut bytes = Vec::new();
1826 write_gtx_header(&mut bytes, 3, 3);
1827 for value in values {
1828 bytes.extend_from_slice(&value.to_be_bytes());
1829 }
1830 bytes
1831 }
1832
1833 fn write_gtx_header(bytes: &mut Vec<u8>, height: i32, width: i32) {
1834 bytes.extend_from_slice(&10.0f64.to_be_bytes());
1835 bytes.extend_from_slice(&20.0f64.to_be_bytes());
1836 bytes.extend_from_slice(&1.0f64.to_be_bytes());
1837 bytes.extend_from_slice(&1.0f64.to_be_bytes());
1838 bytes.extend_from_slice(&height.to_be_bytes());
1839 bytes.extend_from_slice(&width.to_be_bytes());
1840 }
1841
1842 fn gtx_parse_error(bytes: &[u8]) -> String {
1843 match parse_grid_data(GridFormat::Gtx, "test.gtx", bytes) {
1844 Ok(_) => panic!("expected GTX parse failure"),
1845 Err(GridError::Parse(message)) => message,
1846 Err(error) => panic!("expected GTX parse error, got {error}"),
1847 }
1848 }
1849
1850 #[test]
1851 fn gtx_rejects_excessive_dimensions_before_allocation() {
1852 let mut bytes = Vec::new();
1853 write_gtx_header(&mut bytes, 4097, 4097);
1854
1855 let message = gtx_parse_error(&bytes);
1856
1857 assert!(message.contains("GTX cell count"), "{message}");
1858 assert!(message.contains("exceeds limit"), "{message}");
1859 }
1860
1861 #[test]
1862 fn gtx_rejects_oversized_resource_length_before_reading() {
1863 let message = match validate_grid_resource_size(
1864 "oversized.gtx",
1865 GridFormat::Gtx,
1866 MAX_GTX_GRID_BYTES as u64 + 1,
1867 ) {
1868 Ok(()) => panic!("expected GTX resource size failure"),
1869 Err(GridError::Parse(message)) => message,
1870 Err(error) => panic!("expected GTX parse error, got {error}"),
1871 };
1872
1873 assert!(message.contains("maximum Gtx grid size"), "{message}");
1874 }
1875
1876 #[test]
1877 fn gtx_truncated_data_remains_parse_error() {
1878 let mut bytes = Vec::new();
1879 write_gtx_header(&mut bytes, 3, 3);
1880
1881 let message = gtx_parse_error(&bytes);
1882
1883 assert!(message.contains("truncated GTX data"), "{message}");
1884 }
1885
1886 #[test]
1887 fn gtx_grid_samples_bilinear_offsets() {
1888 let bytes = test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
1889 let data = parse_grid_data(GridFormat::Gtx, "test.gtx", &bytes).unwrap();
1890 let GridData::Gtx(grid) = data else {
1891 panic!("expected GTX grid");
1892 };
1893
1894 let sample = grid
1895 .sample(20.5f64.to_radians(), 10.5f64.to_radians())
1896 .unwrap();
1897 assert!((sample.offset_meters - 2.0).abs() < 1e-12);
1898
1899 let wrapped_sample = grid
1900 .sample(
1901 (20.5 + 360.0 * 1_000_000_000_000.0f64).to_radians(),
1902 10.5f64.to_radians(),
1903 )
1904 .unwrap();
1905 assert!((wrapped_sample.offset_meters - 2.0).abs() < 1e-12);
1906
1907 let lower_edge_sample = grid
1908 .sample(
1909 (20.0 - 5e-11f64).to_radians(),
1910 (10.0 - 5e-11f64).to_radians(),
1911 )
1912 .unwrap();
1913 assert!((lower_edge_sample.offset_meters - 0.0).abs() < 1e-12);
1914 }
1915
1916 #[test]
1917 fn gtx_grid_rejects_outside_or_null_cells() {
1918 let bytes = test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, -88.8888, 5.0, 6.0, 7.0, 8.0]);
1919 let data = parse_grid_data(GridFormat::Gtx, "test.gtx", &bytes).unwrap();
1920 let GridData::Gtx(grid) = data else {
1921 panic!("expected GTX grid");
1922 };
1923
1924 let null_err = grid
1925 .sample(20.5f64.to_radians(), 10.5f64.to_radians())
1926 .unwrap_err();
1927 assert!(matches!(null_err, GridError::OutsideCoverage(_)));
1928
1929 let outside_err = grid
1930 .sample(30.0f64.to_radians(), 10.5f64.to_radians())
1931 .unwrap_err();
1932 assert!(matches!(outside_err, GridError::OutsideCoverage(_)));
1933 }
1934
1935 #[test]
1936 fn gtx_grid_rejects_non_finite_coordinates() {
1937 let bytes = test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
1938 let data = parse_grid_data(GridFormat::Gtx, "test.gtx", &bytes).unwrap();
1939 let GridData::Gtx(grid) = data else {
1940 panic!("expected GTX grid");
1941 };
1942
1943 for (lon, lat) in [
1944 (f64::INFINITY, 10.5f64.to_radians()),
1945 (f64::NEG_INFINITY, 10.5f64.to_radians()),
1946 (f64::NAN, 10.5f64.to_radians()),
1947 (20.5f64.to_radians(), f64::INFINITY),
1948 (20.5f64.to_radians(), f64::NAN),
1949 ] {
1950 let err = grid.sample(lon, lat).unwrap_err();
1951 assert!(matches!(err, GridError::OutsideCoverage(_)));
1952 let message = err.to_string();
1953 assert!(message.contains("non-finite"), "{message}");
1954 }
1955 }
1956
1957 #[test]
1958 fn app_grid_provider_can_override_embedded_grid() {
1959 let definition_calls = Arc::new(AtomicUsize::new(0));
1960 let load_calls = Arc::new(AtomicUsize::new(0));
1961 let provider = TrackingGridProvider {
1962 override_definition: true,
1963 definition_calls: Arc::clone(&definition_calls),
1964 load_calls: Arc::clone(&load_calls),
1965 };
1966 let runtime = GridRuntime::new(Some(Arc::new(provider)));
1967
1968 let handle = runtime
1969 .resolve_handle(&test_grid_definition())
1970 .expect("grid should resolve");
1971
1972 assert_eq!(handle.definition().name, "custom override");
1973 assert_eq!(definition_calls.load(Ordering::SeqCst), 1);
1974 assert_eq!(load_calls.load(Ordering::SeqCst), 1);
1975 }
1976
1977 #[test]
1978 fn app_grid_provider_falls_back_to_embedded_grid() {
1979 let definition_calls = Arc::new(AtomicUsize::new(0));
1980 let load_calls = Arc::new(AtomicUsize::new(0));
1981 let provider = TrackingGridProvider {
1982 override_definition: false,
1983 definition_calls: Arc::clone(&definition_calls),
1984 load_calls: Arc::clone(&load_calls),
1985 };
1986 let runtime = GridRuntime::new(Some(Arc::new(provider)));
1987
1988 let handle = runtime
1989 .resolve_handle(&test_grid_definition())
1990 .expect("embedded grid should remain available");
1991
1992 assert_eq!(handle.definition().name, "ntv2_0.gsb");
1993 assert_eq!(definition_calls.load(Ordering::SeqCst), 1);
1994 assert_eq!(load_calls.load(Ordering::SeqCst), 1);
1995 }
1996}