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