Skip to main content

proj_core/
grid.rs

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 horizontal datum-shift grid (`.gsb`).
26    Ntv2,
27    /// NOAA/VDatum binary GTX vertical offset grid (`.gtx`).
28    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    /// Vertical offset in meters at the sampled horizontal position.
51    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    /// Parse a grid resource into a handle.
84    ///
85    /// Custom [`GridProvider`] implementations can use this constructor after
86    /// loading bytes from their own package, object store, or manifest.
87    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    // Retain roots that do not exist yet so callers can construct a provider
282    // before mounting or creating the grid directory.
283    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}