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#[cfg(feature = "geotiff")]
26const MAX_GEOTIFF_IFDS: usize = 4_096;
27#[cfg(feature = "geotiff")]
28const MAX_GEOTIFF_CELLS_PER_IMAGE: usize = 16_777_216;
29#[cfg(feature = "geotiff")]
30const MAX_GEOTIFF_TOTAL_CELLS: usize = 16_777_216;
31#[cfg(feature = "geotiff")]
32const MAX_GEOTIFF_BANDS: usize = 4;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum GridFormat {
36    /// NTv2 horizontal datum-shift grid (`.gsb`).
37    Ntv2,
38    /// NOAA/VDatum binary GTX vertical offset grid (`.gtx`).
39    Gtx,
40    /// PROJ-format GeoTIFF/COG grid (`.tif`), as distributed on the PROJ CDN.
41    ///
42    /// The `TYPE` GDAL metadata item selects horizontal (NTv2-equivalent
43    /// latitude/longitude offsets) or vertical (geoid undulation) semantics;
44    /// both are decoded into the same internal representation as the binary
45    /// NTv2/GTX formats. Requires the `geotiff` crate feature.
46    GeoTiff,
47    Unsupported,
48}
49
50#[derive(Debug, Clone, PartialEq)]
51pub struct GridDefinition {
52    pub id: GridId,
53    pub name: String,
54    pub format: GridFormat,
55    pub interpolation: GridInterpolation,
56    pub area_of_use: Option<AreaOfUse>,
57    pub resource_names: SmallVec<[String; 2]>,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61struct GridRuntimeCacheKey {
62    id: GridId,
63    name: String,
64    format: GridFormat,
65    interpolation: GridInterpolation,
66    area_of_use: Option<GridAreaCacheKey>,
67    resource_names: SmallVec<[String; 2]>,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71struct GridAreaCacheKey {
72    west_bits: u64,
73    south_bits: u64,
74    east_bits: u64,
75    north_bits: u64,
76    name: String,
77}
78
79impl From<&GridDefinition> for GridRuntimeCacheKey {
80    fn from(grid: &GridDefinition) -> Self {
81        Self {
82            id: grid.id,
83            name: grid.name.clone(),
84            format: grid.format,
85            interpolation: grid.interpolation,
86            area_of_use: grid.area_of_use.as_ref().map(|area| GridAreaCacheKey {
87                west_bits: area.west.to_bits(),
88                south_bits: area.south.to_bits(),
89                east_bits: area.east.to_bits(),
90                north_bits: area.north.to_bits(),
91                name: area.name.clone(),
92            }),
93            resource_names: grid.resource_names.clone(),
94        }
95    }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
99pub struct GridSample {
100    pub lon_shift_radians: f64,
101    pub lat_shift_radians: f64,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq)]
105pub struct VerticalGridSample {
106    /// Vertical offset in meters at the sampled horizontal position.
107    pub offset_meters: f64,
108}
109
110#[derive(Debug, Error, Clone)]
111#[non_exhaustive]
112pub enum GridError {
113    #[error("grid not found: {0}")]
114    NotFound(String),
115    #[error("grid resource unavailable: {0}")]
116    Unavailable(String),
117    #[error("grid parse error: {0}")]
118    Parse(String),
119    #[error("grid point outside coverage: {0}")]
120    OutsideCoverage(String),
121    #[error("unsupported grid format: {0}")]
122    UnsupportedFormat(String),
123}
124
125pub trait GridProvider: Send + Sync {
126    fn definition(
127        &self,
128        grid: &GridDefinition,
129    ) -> std::result::Result<Option<GridDefinition>, GridError>;
130    fn load(&self, grid: &GridDefinition) -> std::result::Result<Option<GridHandle>, GridError>;
131}
132
133#[derive(Clone)]
134pub struct GridHandle {
135    definition: GridDefinition,
136    data: Arc<CachedGridData>,
137}
138
139impl std::fmt::Debug for GridHandle {
140    /// Summary form: grid data can be hundreds of megabytes, so print the
141    /// definition and content checksum instead of the samples.
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        f.debug_struct("GridHandle")
144            .field("definition", &self.definition)
145            .field("checksum", &self.data.checksum)
146            .finish_non_exhaustive()
147    }
148}
149
150impl GridHandle {
151    /// Parse a grid resource into a handle.
152    ///
153    /// Custom [`GridProvider`] implementations can use this constructor after
154    /// loading bytes from their own package, object store, or manifest.
155    pub fn from_bytes(
156        definition: GridDefinition,
157        bytes: &[u8],
158    ) -> std::result::Result<Self, GridError> {
159        Ok(Self {
160            data: Arc::new(parse_cached_grid_data(
161                definition.format,
162                &definition.name,
163                bytes,
164            )?),
165            definition,
166        })
167    }
168
169    pub fn definition(&self) -> &GridDefinition {
170        &self.definition
171    }
172
173    pub fn checksum(&self) -> &str {
174        &self.data.checksum
175    }
176
177    pub fn sample(
178        &self,
179        lon_radians: f64,
180        lat_radians: f64,
181    ) -> std::result::Result<GridSample, GridError> {
182        match &self.data.data {
183            GridData::Ntv2(set) => set.sample(lon_radians, lat_radians),
184            GridData::Gtx(_) => Err(GridError::UnsupportedFormat(format!(
185                "{} is a vertical grid",
186                self.definition.name
187            ))),
188        }
189    }
190
191    pub fn sample_vertical_offset_meters(
192        &self,
193        lon_radians: f64,
194        lat_radians: f64,
195    ) -> std::result::Result<VerticalGridSample, GridError> {
196        match &self.data.data {
197            GridData::Gtx(grid) => grid.sample(lon_radians, lat_radians),
198            GridData::Ntv2(_) => Err(GridError::UnsupportedFormat(format!(
199                "{} is a horizontal grid",
200                self.definition.name
201            ))),
202        }
203    }
204
205    pub fn apply(
206        &self,
207        lon_radians: f64,
208        lat_radians: f64,
209        direction: GridShiftDirection,
210    ) -> std::result::Result<(f64, f64), GridError> {
211        match &self.data.data {
212            GridData::Ntv2(set) => set.apply(lon_radians, lat_radians, direction),
213            GridData::Gtx(_) => Err(GridError::UnsupportedFormat(format!(
214                "{} is a vertical grid",
215                self.definition.name
216            ))),
217        }
218    }
219}
220
221pub(crate) struct GridRuntime {
222    providers: Vec<Arc<dyn GridProvider>>,
223    definition_cache: Mutex<HashMap<GridRuntimeCacheKey, GridDefinition>>,
224    handle_cache: Mutex<HashMap<GridRuntimeCacheKey, GridHandle>>,
225}
226
227impl GridRuntime {
228    pub(crate) fn new(app_provider: Option<Arc<dyn GridProvider>>) -> Self {
229        let mut providers: Vec<Arc<dyn GridProvider>> = Vec::with_capacity(2);
230        if let Some(provider) = app_provider {
231            providers.push(provider);
232        }
233        providers.push(Arc::new(EmbeddedGridProvider));
234        Self {
235            providers,
236            definition_cache: Mutex::new(HashMap::new()),
237            handle_cache: Mutex::new(HashMap::new()),
238        }
239    }
240
241    pub(crate) fn resolve_definition(
242        &self,
243        grid: &GridDefinition,
244    ) -> std::result::Result<GridDefinition, GridError> {
245        let cache_key = grid_runtime_cache_key(grid);
246        if let Some(cached) = self
247            .definition_cache
248            .lock()
249            .expect("grid definition cache poisoned")
250            .get(&cache_key)
251            .cloned()
252        {
253            return Ok(cached);
254        }
255
256        for provider in &self.providers {
257            if let Some(definition) = provider.definition(grid)? {
258                self.definition_cache
259                    .lock()
260                    .expect("grid definition cache poisoned")
261                    .insert(cache_key, definition.clone());
262                return Ok(definition);
263            }
264        }
265
266        Err(GridError::Unavailable(grid.name.clone()))
267    }
268
269    pub(crate) fn resolve_handle(
270        &self,
271        grid: &GridDefinition,
272    ) -> std::result::Result<GridHandle, GridError> {
273        let cache_key = grid_runtime_cache_key(grid);
274        if let Some(cached) = self
275            .handle_cache
276            .lock()
277            .expect("grid handle cache poisoned")
278            .get(&cache_key)
279            .cloned()
280        {
281            return Ok(cached);
282        }
283
284        let definition = self.resolve_definition(grid)?;
285        for provider in &self.providers {
286            if let Some(handle) = provider.load(&definition)? {
287                self.handle_cache
288                    .lock()
289                    .expect("grid handle cache poisoned")
290                    .insert(cache_key, handle.clone());
291                return Ok(handle);
292            }
293        }
294
295        Err(GridError::Unavailable(definition.name))
296    }
297}
298
299fn grid_runtime_cache_key(grid: &GridDefinition) -> GridRuntimeCacheKey {
300    grid.into()
301}
302
303#[derive(Default)]
304pub struct EmbeddedGridProvider;
305
306impl GridProvider for EmbeddedGridProvider {
307    fn definition(
308        &self,
309        grid: &GridDefinition,
310    ) -> std::result::Result<Option<GridDefinition>, GridError> {
311        if embedded_grid_resource(&grid.resource_names).is_some() {
312            return Ok(Some(grid.clone()));
313        }
314        Ok(None)
315    }
316
317    fn load(&self, grid: &GridDefinition) -> std::result::Result<Option<GridHandle>, GridError> {
318        let Some((resource_name, bytes)) = embedded_grid_resource(&grid.resource_names) else {
319            return Ok(None);
320        };
321
322        let key = GridDataCacheKey::new(grid.format, resource_name);
323        let data = cached_grid_data(embedded_grid_data_cache(), key, || {
324            parse_cached_grid_data(grid.format, &grid.name, bytes)
325        })?;
326
327        Ok(Some(GridHandle {
328            definition: grid.clone(),
329            data,
330        }))
331    }
332}
333
334pub struct FilesystemGridProvider {
335    roots: Mutex<Vec<FilesystemGridRoot>>,
336    location_cache: Mutex<HashMap<GridRuntimeCacheKey, FilesystemGridLocation>>,
337    data_cache: GridDataCache,
338    #[cfg(test)]
339    locate_searches: std::sync::atomic::AtomicUsize,
340}
341
342enum FilesystemGridRoot {
343    Canonical(PathBuf),
344    // Retain roots that do not exist yet so callers can construct a provider
345    // before mounting or creating the grid directory.
346    Unresolved(PathBuf),
347}
348
349#[derive(Clone)]
350struct FilesystemGridLocation {
351    root: PathBuf,
352    path: PathBuf,
353}
354
355impl FilesystemGridProvider {
356    pub fn new<I>(roots: I) -> Self
357    where
358        I: IntoIterator<Item = PathBuf>,
359    {
360        Self {
361            roots: Mutex::new(
362                roots
363                    .into_iter()
364                    .map(|root| match root.canonicalize() {
365                        Ok(canonical_root) => FilesystemGridRoot::Canonical(canonical_root),
366                        Err(_) => FilesystemGridRoot::Unresolved(root),
367                    })
368                    .collect(),
369            ),
370            location_cache: Mutex::new(HashMap::new()),
371            data_cache: Mutex::new(HashMap::new()),
372            #[cfg(test)]
373            locate_searches: std::sync::atomic::AtomicUsize::new(0),
374        }
375    }
376
377    fn locate(&self, grid: &GridDefinition) -> Option<FilesystemGridLocation> {
378        let cache_key = grid_runtime_cache_key(grid);
379        let cached_location = {
380            self.location_cache
381                .lock()
382                .expect("filesystem grid location cache poisoned")
383                .get(&cache_key)
384                .cloned()
385        };
386        if let Some(location) = cached_location {
387            if let Some(validated) = self.revalidate_location(&location) {
388                if validated.path != location.path {
389                    self.location_cache
390                        .lock()
391                        .expect("filesystem grid location cache poisoned")
392                        .insert(cache_key, validated.clone());
393                }
394                return Some(validated);
395            }
396
397            self.location_cache
398                .lock()
399                .expect("filesystem grid location cache poisoned")
400                .remove(&cache_key);
401        }
402
403        let location = self.locate_uncached(grid)?;
404        self.location_cache
405            .lock()
406            .expect("filesystem grid location cache poisoned")
407            .insert(cache_key, location.clone());
408        Some(location)
409    }
410
411    fn locate_uncached(&self, grid: &GridDefinition) -> Option<FilesystemGridLocation> {
412        #[cfg(test)]
413        self.locate_searches
414            .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
415
416        let safe_resource_names = grid
417            .resource_names
418            .iter()
419            .filter(|name| is_safe_grid_resource_name(name))
420            .collect::<Vec<_>>();
421        if safe_resource_names.is_empty() {
422            return None;
423        }
424
425        for root in self.canonical_roots_for_lookup() {
426            for name in &safe_resource_names {
427                let candidate = root.join(name);
428                let Ok(canonical_candidate) = candidate.canonicalize() else {
429                    continue;
430                };
431                if canonical_candidate.starts_with(&root) && canonical_candidate.is_file() {
432                    return Some(FilesystemGridLocation {
433                        root,
434                        path: canonical_candidate,
435                    });
436                }
437            }
438        }
439        None
440    }
441
442    fn revalidate_location(
443        &self,
444        location: &FilesystemGridLocation,
445    ) -> Option<FilesystemGridLocation> {
446        let Ok(canonical_path) = location.path.canonicalize() else {
447            return None;
448        };
449        if !canonical_path.starts_with(&location.root) || !canonical_path.is_file() {
450            return None;
451        }
452        Some(FilesystemGridLocation {
453            root: location.root.clone(),
454            path: canonical_path,
455        })
456    }
457
458    fn canonical_roots_for_lookup(&self) -> Vec<PathBuf> {
459        let mut roots = self.roots.lock().expect("filesystem grid roots poisoned");
460        let mut canonical_roots = Vec::with_capacity(roots.len());
461        for root in roots.iter_mut() {
462            match root {
463                FilesystemGridRoot::Canonical(canonical_root) => {
464                    canonical_roots.push(canonical_root.clone());
465                }
466                FilesystemGridRoot::Unresolved(unresolved_root) => {
467                    let Ok(canonical_root) = unresolved_root.canonicalize() else {
468                        continue;
469                    };
470                    *root = FilesystemGridRoot::Canonical(canonical_root.clone());
471                    canonical_roots.push(canonical_root);
472                }
473            }
474        }
475        canonical_roots
476    }
477}
478
479impl GridProvider for FilesystemGridProvider {
480    fn definition(
481        &self,
482        grid: &GridDefinition,
483    ) -> std::result::Result<Option<GridDefinition>, GridError> {
484        if self.locate(grid).is_some() {
485            return Ok(Some(grid.clone()));
486        }
487        Ok(None)
488    }
489
490    fn load(&self, grid: &GridDefinition) -> std::result::Result<Option<GridHandle>, GridError> {
491        let Some(location) = self.locate(grid) else {
492            return Ok(None);
493        };
494
495        let key = GridDataCacheKey::new(grid.format, location.path.to_string_lossy());
496        let data = cached_grid_data(&self.data_cache, key, || {
497            let bytes = read_filesystem_grid_resource_bytes(&location, grid.format)?;
498            parse_cached_grid_data(grid.format, &grid.name, &bytes)
499        })?;
500
501        Ok(Some(GridHandle {
502            definition: grid.clone(),
503            data,
504        }))
505    }
506}
507
508fn is_safe_grid_resource_name(name: &str) -> bool {
509    let path = Path::new(name);
510    if path.as_os_str().is_empty() {
511        return false;
512    }
513    path.components()
514        .all(|component| matches!(component, Component::Normal(_)))
515}
516
517fn read_filesystem_grid_resource_bytes(
518    location: &FilesystemGridLocation,
519    format: GridFormat,
520) -> std::result::Result<Vec<u8>, GridError> {
521    let canonical_path = location
522        .path
523        .canonicalize()
524        .map_err(|err| GridError::Unavailable(format!("{}: {err}", location.path.display())))?;
525    if canonical_path != location.path || !canonical_path.starts_with(&location.root) {
526        return Err(GridError::Unavailable(format!(
527            "{} is no longer contained by {}",
528            location.path.display(),
529            location.root.display()
530        )));
531    }
532
533    let metadata = std::fs::metadata(&canonical_path)
534        .map_err(|err| GridError::Unavailable(format!("{}: {err}", canonical_path.display())))?;
535    if !metadata.is_file() {
536        return Err(GridError::Unavailable(format!(
537            "{} is not a regular file",
538            canonical_path.display()
539        )));
540    }
541
542    let file = open_filesystem_grid_resource_file(location, &canonical_path)?;
543    let opened_metadata = file
544        .metadata()
545        .map_err(|err| GridError::Unavailable(format!("{}: {err}", canonical_path.display())))?;
546    ensure_same_grid_resource_file(&canonical_path, &metadata, &opened_metadata)?;
547
548    read_grid_resource_file(file, &canonical_path, format)
549}
550
551#[cfg(unix)]
552fn open_filesystem_grid_resource_file(
553    location: &FilesystemGridLocation,
554    canonical_path: &Path,
555) -> std::result::Result<std::fs::File, GridError> {
556    use rustix::fs::{open, openat, Mode, OFlags};
557
558    let relative_path = canonical_path.strip_prefix(&location.root).map_err(|_| {
559        GridError::Unavailable(format!(
560            "{} is no longer contained by {}",
561            canonical_path.display(),
562            location.root.display()
563        ))
564    })?;
565
566    let mut components = relative_path.components().peekable();
567    let Some(_) = components.peek() else {
568        return Err(GridError::Unavailable(format!(
569            "{} is not a grid file path",
570            canonical_path.display()
571        )));
572    };
573
574    let directory_flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
575    let file_flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
576    let empty_mode = Mode::empty();
577    let mut dir = open(&location.root, directory_flags, empty_mode)
578        .map_err(|err| GridError::Unavailable(format!("{}: {err}", location.root.display())))?;
579
580    while let Some(component) = components.next() {
581        let Component::Normal(name) = component else {
582            return Err(GridError::Unavailable(format!(
583                "{} is not a normal relative grid path",
584                canonical_path.display()
585            )));
586        };
587
588        if components.peek().is_some() {
589            dir = openat(&dir, name, directory_flags, empty_mode).map_err(|err| {
590                GridError::Unavailable(format!("{}: {err}", canonical_path.display()))
591            })?;
592        } else {
593            let file = openat(&dir, name, file_flags, empty_mode).map_err(|err| {
594                GridError::Unavailable(format!("{}: {err}", canonical_path.display()))
595            })?;
596            return Ok(std::fs::File::from(file));
597        }
598    }
599
600    Err(GridError::Unavailable(format!(
601        "{} is not a grid file path",
602        canonical_path.display()
603    )))
604}
605
606#[cfg(not(unix))]
607fn open_filesystem_grid_resource_file(
608    _location: &FilesystemGridLocation,
609    canonical_path: &Path,
610) -> std::result::Result<std::fs::File, GridError> {
611    std::fs::File::open(canonical_path)
612        .map_err(|err| GridError::Unavailable(format!("{}: {err}", canonical_path.display())))
613}
614
615fn read_grid_resource_file(
616    file: std::fs::File,
617    path: &Path,
618    format: GridFormat,
619) -> std::result::Result<Vec<u8>, GridError> {
620    let Some(max_bytes) = max_grid_resource_bytes(format) else {
621        return Err(GridError::UnsupportedFormat(format!(
622            "{}: {format:?}",
623            path.display()
624        )));
625    };
626    read_bounded_grid_resource_file(file, path, format, max_bytes)
627}
628
629#[cfg(test)]
630fn read_bounded_grid_resource_bytes(
631    path: &Path,
632    format: GridFormat,
633    max_bytes: usize,
634) -> std::result::Result<Vec<u8>, GridError> {
635    let file = std::fs::File::open(path)
636        .map_err(|err| GridError::Unavailable(format!("{}: {err}", path.display())))?;
637    read_bounded_grid_resource_file(file, path, format, max_bytes)
638}
639
640fn read_bounded_grid_resource_file(
641    file: std::fs::File,
642    path: &Path,
643    format: GridFormat,
644    max_bytes: usize,
645) -> std::result::Result<Vec<u8>, GridError> {
646    let read_limit = u64::try_from(max_bytes)
647        .unwrap_or(u64::MAX)
648        .saturating_add(1);
649    let mut reader = file.take(read_limit);
650    let mut bytes = Vec::with_capacity(max_bytes.min(8192));
651    reader
652        .read_to_end(&mut bytes)
653        .map_err(|err| GridError::Unavailable(format!("{}: {err}", path.display())))?;
654
655    if bytes.len() > max_bytes {
656        return Err(GridError::Parse(format!(
657            "{} exceeds maximum {format:?} grid size of {max_bytes} bytes",
658            path.display()
659        )));
660    }
661
662    Ok(bytes)
663}
664
665#[cfg(unix)]
666fn ensure_same_grid_resource_file(
667    path: &Path,
668    expected: &std::fs::Metadata,
669    opened: &std::fs::Metadata,
670) -> std::result::Result<(), GridError> {
671    use std::os::unix::fs::MetadataExt;
672
673    if expected.dev() != opened.dev() || expected.ino() != opened.ino() {
674        return Err(GridError::Unavailable(format!(
675            "{} changed while opening",
676            path.display()
677        )));
678    }
679    Ok(())
680}
681
682#[cfg(not(unix))]
683fn ensure_same_grid_resource_file(
684    _path: &Path,
685    _expected: &std::fs::Metadata,
686    _opened: &std::fs::Metadata,
687) -> std::result::Result<(), GridError> {
688    Ok(())
689}
690
691fn validate_grid_resource_size(
692    resource: impl std::fmt::Display,
693    format: GridFormat,
694    len: u64,
695) -> std::result::Result<(), GridError> {
696    if let Some(max_bytes) = max_grid_resource_bytes(format) {
697        let max_bytes_u64 = u64::try_from(max_bytes).unwrap_or(u64::MAX);
698        if len > max_bytes_u64 {
699            return Err(GridError::Parse(format!(
700                "{resource} exceeds maximum {format:?} grid size of {max_bytes} bytes"
701            )));
702        }
703    }
704    Ok(())
705}
706
707fn max_grid_resource_bytes(format: GridFormat) -> Option<usize> {
708    match format {
709        GridFormat::Ntv2 => Some(MAX_NTV2_GRID_BYTES),
710        GridFormat::Gtx => Some(MAX_GTX_GRID_BYTES),
711        GridFormat::GeoTiff => Some(MAX_GEOTIFF_GRID_BYTES),
712        GridFormat::Unsupported => None,
713    }
714}
715
716enum GridData {
717    Ntv2(Ntv2GridSet),
718    Gtx(GtxGrid),
719}
720
721struct CachedGridData {
722    data: GridData,
723    checksum: String,
724}
725
726type GridDataCache = Mutex<HashMap<GridDataCacheKey, Arc<GridDataCacheSlot>>>;
727
728struct GridDataCacheSlot {
729    state: Mutex<GridDataCacheState>,
730    ready: Condvar,
731}
732
733enum GridDataCacheState {
734    Loading,
735    Ready(Arc<CachedGridData>),
736    Failed(GridError),
737}
738
739impl GridDataCacheSlot {
740    fn loading() -> Self {
741        Self {
742            state: Mutex::new(GridDataCacheState::Loading),
743            ready: Condvar::new(),
744        }
745    }
746}
747
748#[derive(Debug, Clone, PartialEq, Eq, Hash)]
749struct GridDataCacheKey {
750    format: GridFormat,
751    resource: String,
752}
753
754impl GridDataCacheKey {
755    fn new(format: GridFormat, resource: impl AsRef<str>) -> Self {
756        Self {
757            format,
758            resource: resource.as_ref().to_string(),
759        }
760    }
761}
762
763fn embedded_grid_data_cache() -> &'static GridDataCache {
764    static CACHE: OnceLock<GridDataCache> = OnceLock::new();
765    CACHE.get_or_init(|| Mutex::new(HashMap::new()))
766}
767
768fn cached_grid_data(
769    cache: &GridDataCache,
770    key: GridDataCacheKey,
771    parse: impl FnOnce() -> std::result::Result<CachedGridData, GridError>,
772) -> std::result::Result<Arc<CachedGridData>, GridError> {
773    let (slot, should_load) = {
774        let mut cache = cache.lock().expect("grid data cache poisoned");
775        if let Some(slot) = cache.get(&key) {
776            (Arc::clone(slot), false)
777        } else {
778            let slot = Arc::new(GridDataCacheSlot::loading());
779            cache.insert(key.clone(), Arc::clone(&slot));
780            (slot, true)
781        }
782    };
783
784    if should_load {
785        let result = parse().map(Arc::new);
786        if result.is_err() {
787            let mut cache = cache.lock().expect("grid data cache poisoned");
788            let should_remove = cache
789                .get(&key)
790                .map(|cached_slot| Arc::ptr_eq(cached_slot, &slot))
791                .unwrap_or(false);
792            if should_remove {
793                cache.remove(&key);
794            }
795        }
796
797        let mut state = slot.state.lock().expect("grid data cache slot poisoned");
798        match &result {
799            Ok(data) => *state = GridDataCacheState::Ready(Arc::clone(data)),
800            Err(error) => *state = GridDataCacheState::Failed(error.clone()),
801        }
802        slot.ready.notify_all();
803        return result;
804    }
805
806    let mut state = slot.state.lock().expect("grid data cache slot poisoned");
807    loop {
808        match &*state {
809            GridDataCacheState::Ready(data) => return Ok(Arc::clone(data)),
810            GridDataCacheState::Failed(error) => return Err(error.clone()),
811            GridDataCacheState::Loading => {
812                state = slot
813                    .ready
814                    .wait(state)
815                    .expect("grid data cache slot poisoned");
816            }
817        }
818    }
819}
820
821fn parse_grid_data(
822    format: GridFormat,
823    name: &str,
824    bytes: &[u8],
825) -> std::result::Result<GridData, GridError> {
826    validate_grid_resource_size(name, format, u64::try_from(bytes.len()).unwrap_or(u64::MAX))?;
827
828    match format {
829        GridFormat::Ntv2 => Ok(GridData::Ntv2(Ntv2GridSet::parse(bytes)?)),
830        GridFormat::Gtx => Ok(GridData::Gtx(GtxGrid::parse(bytes)?)),
831        GridFormat::GeoTiff => parse_geotiff_grid_data(name, bytes),
832        GridFormat::Unsupported => Err(GridError::UnsupportedFormat(name.into())),
833    }
834}
835
836#[cfg(not(feature = "geotiff"))]
837fn parse_geotiff_grid_data(name: &str, _bytes: &[u8]) -> std::result::Result<GridData, GridError> {
838    Err(GridError::UnsupportedFormat(format!(
839        "{name}: GeoTIFF grid support requires the `geotiff` crate feature"
840    )))
841}
842
843#[cfg(feature = "geotiff")]
844fn parse_geotiff_grid_data(name: &str, bytes: &[u8]) -> std::result::Result<GridData, GridError> {
845    geotiff::parse(name, bytes)
846}
847
848/// Decode PROJ-format GeoTIFF grids into the same internal representation as the
849/// binary NTv2 (`Ntv2GridSet`) and GTX (`GtxGrid`) formats, so all sampling,
850/// bilinear interpolation, nested-grid selection, and inverse iteration is
851/// shared with those code paths.
852///
853/// PROJ stores its grids as cloud-optimized GeoTIFFs: a horizontal datum-shift
854/// grid carries `latitude_offset`/`longitude_offset` bands in arc-seconds (with
855/// nested finer subgrids as additional IFDs), and a geoid grid carries a single
856/// `geoid_undulation` band in metres. The grid role is taken from the `TYPE`
857/// item of the `GDAL_METADATA` tag.
858#[cfg(feature = "geotiff")]
859mod geotiff {
860    use super::{
861        GridData, GridError, GridExtent, GtxGrid, Ntv2Grid, Ntv2GridSet, MAX_GEOTIFF_BANDS,
862        MAX_GEOTIFF_CELLS_PER_IMAGE, MAX_GEOTIFF_IFDS, MAX_GEOTIFF_TOTAL_CELLS,
863    };
864    use geotiff_reader::{GeoTiffFile, GeoTiffOpenOptions};
865    use std::f64::consts::PI;
866    use tiff_core::TagValue;
867
868    const TIFFTAG_MODEL_PIXEL_SCALE: u16 = 33550;
869    const TIFFTAG_MODEL_TIEPOINT: u16 = 33922;
870    const TIFFTAG_GDAL_METADATA: u16 = 42112;
871    const ARCSEC_TO_RAD: f64 = PI / 180.0 / 3600.0;
872    const DEG_TO_RAD: f64 = PI / 180.0;
873
874    #[derive(Clone, Copy)]
875    enum Kind {
876        Horizontal,
877        Vertical,
878    }
879
880    struct ImageMetadata {
881        west_node_deg: f64,
882        north_node_deg: f64,
883        scale_lon_deg: f64,
884        scale_lat_deg: f64,
885        width: usize,
886        height: usize,
887        cell_count: usize,
888    }
889
890    /// One decoded image (IFD): node-origin georeferencing plus per-band values
891    /// laid out row-major, north-to-south (TIFF raster order).
892    struct Image {
893        west_node_deg: f64,
894        north_node_deg: f64,
895        scale_lon_deg: f64,
896        scale_lat_deg: f64,
897        width: usize,
898        height: usize,
899        bands: Vec<Vec<f64>>,
900    }
901
902    pub(super) fn parse(name: &str, bytes: &[u8]) -> Result<GridData, GridError> {
903        let mut options = GeoTiffOpenOptions::default();
904        options.parse_budgets.max_ifds = MAX_GEOTIFF_IFDS;
905        let file = GeoTiffFile::from_bytes_with_options(bytes.to_vec(), options)
906            .map_err(|err| GridError::Parse(format!("{name}: {err}")))?;
907        let tiff = file.tiff();
908        let ifd_count = tiff.ifd_count();
909        if ifd_count == 0 {
910            return Err(GridError::Parse(format!("{name}: no images in GeoTIFF")));
911        }
912        if ifd_count > MAX_GEOTIFF_IFDS {
913            return Err(GridError::Parse(format!(
914                "{name}: GeoTIFF IFD count {ifd_count} exceeds limit {MAX_GEOTIFF_IFDS}"
915            )));
916        }
917
918        let base_index = file.base_ifd_index();
919        let base_ifd = tiff
920            .ifd(base_index)
921            .map_err(|err| GridError::Parse(format!("{name}: {err}")))?;
922        let kind = grid_kind(
923            base_ifd.tag(TIFFTAG_GDAL_METADATA).map(|tag| &tag.value),
924            name,
925        )?;
926
927        match kind {
928            Kind::Vertical => {
929                let metadata = read_image_metadata(&file, base_index, kind, name)?;
930                let image = read_image(&file, base_index, kind, &metadata, name)?;
931                Ok(GridData::Gtx(build_gtx(&image)))
932            }
933            Kind::Horizontal => {
934                let mut metadata = Vec::with_capacity(ifd_count);
935                let mut total_cells = 0usize;
936                for index in 0..ifd_count {
937                    let image_metadata = read_image_metadata(&file, index, kind, name)?;
938                    total_cells = total_cells
939                        .checked_add(image_metadata.cell_count)
940                        .ok_or_else(|| {
941                            GridError::Parse(format!("{name}: GeoTIFF total cell count overflow"))
942                        })?;
943                    if total_cells > MAX_GEOTIFF_TOTAL_CELLS {
944                        return Err(GridError::Parse(format!(
945                            "{name}: GeoTIFF total cell count {total_cells} exceeds limit {MAX_GEOTIFF_TOTAL_CELLS}"
946                        )));
947                    }
948                    metadata.push(image_metadata);
949                }
950
951                let mut images = Vec::with_capacity(metadata.len());
952                for (index, image_metadata) in metadata.iter().enumerate() {
953                    images.push(read_image(&file, index, kind, image_metadata, name)?);
954                }
955                Ok(GridData::Ntv2(build_ntv2(&images, name)?))
956            }
957        }
958    }
959
960    fn grid_kind(metadata: Option<&TagValue>, name: &str) -> Result<Kind, GridError> {
961        let text = match metadata {
962            Some(TagValue::Ascii(text)) => text.to_ascii_uppercase(),
963            _ => String::new(),
964        };
965        if text.contains("VERTICAL") {
966            Ok(Kind::Vertical)
967        } else if text.contains("HORIZONTAL") {
968            Ok(Kind::Horizontal)
969        } else {
970            Err(GridError::Parse(format!(
971                "{name}: GeoTIFF grid is missing a recognised GDAL `TYPE` (HORIZONTAL_OFFSET / VERTICAL_OFFSET)"
972            )))
973        }
974    }
975
976    fn read_image_metadata(
977        file: &GeoTiffFile,
978        index: usize,
979        kind: Kind,
980        name: &str,
981    ) -> Result<ImageMetadata, GridError> {
982        let tiff = file.tiff();
983        let ifd = tiff
984            .ifd(index)
985            .map_err(|err| GridError::Parse(format!("{name}: {err}")))?;
986        let width = ifd.width() as usize;
987        let height = ifd.height() as usize;
988        if width < 2 || height < 2 {
989            return Err(GridError::Parse(format!(
990                "{name}: GeoTIFF image {index} is smaller than 2x2"
991            )));
992        }
993        if width > MAX_GEOTIFF_CELLS_PER_IMAGE {
994            return Err(GridError::Parse(format!(
995                "{name}: GeoTIFF image {index} width {width} exceeds limit {MAX_GEOTIFF_CELLS_PER_IMAGE}"
996            )));
997        }
998        if height > MAX_GEOTIFF_CELLS_PER_IMAGE {
999            return Err(GridError::Parse(format!(
1000                "{name}: GeoTIFF image {index} height {height} exceeds limit {MAX_GEOTIFF_CELLS_PER_IMAGE}"
1001            )));
1002        }
1003        let cell_count = width.checked_mul(height).ok_or_else(|| {
1004            GridError::Parse(format!("{name}: GeoTIFF image {index} cell count overflow"))
1005        })?;
1006        if cell_count > MAX_GEOTIFF_CELLS_PER_IMAGE {
1007            return Err(GridError::Parse(format!(
1008                "{name}: GeoTIFF image {index} cell count {cell_count} exceeds limit {MAX_GEOTIFF_CELLS_PER_IMAGE}"
1009            )));
1010        }
1011
1012        let scale = doubles(ifd.tag(TIFFTAG_MODEL_PIXEL_SCALE).map(|tag| &tag.value))
1013            .ok_or_else(|| GridError::Parse(format!("{name}: missing ModelPixelScale")))?;
1014        let tiepoint = doubles(ifd.tag(TIFFTAG_MODEL_TIEPOINT).map(|tag| &tag.value))
1015            .ok_or_else(|| GridError::Parse(format!("{name}: missing ModelTiepoint")))?;
1016        if scale.len() < 2 || tiepoint.len() < 6 {
1017            return Err(GridError::Parse(format!(
1018                "{name}: malformed GeoTIFF georeferencing tags"
1019            )));
1020        }
1021        let scale_lon_deg = scale[0];
1022        let scale_lat_deg = scale[1];
1023        // Tiepoint maps raster (i, j) -> model (x, y). PROJ grids use a Point
1024        // raster, so the (0, 0) tiepoint is the node coordinate directly.
1025        let west_node_deg = tiepoint[3] - tiepoint[0] * scale_lon_deg;
1026        let north_node_deg = tiepoint[4] + tiepoint[1] * scale_lat_deg;
1027        if !(scale_lon_deg.is_finite()
1028            && scale_lat_deg.is_finite()
1029            && scale_lon_deg > 0.0
1030            && scale_lat_deg > 0.0
1031            && west_node_deg.is_finite()
1032            && north_node_deg.is_finite())
1033        {
1034            return Err(GridError::Parse(format!(
1035                "{name}: invalid GeoTIFF georeferencing"
1036            )));
1037        }
1038
1039        let band_count = ifd.samples_per_pixel() as usize;
1040        if band_count == 0 {
1041            return Err(GridError::Parse(format!(
1042                "{name}: GeoTIFF image {index} has no bands"
1043            )));
1044        }
1045        if band_count > MAX_GEOTIFF_BANDS {
1046            return Err(GridError::Parse(format!(
1047                "{name}: GeoTIFF image {index} band count {band_count} exceeds limit {MAX_GEOTIFF_BANDS}"
1048            )));
1049        }
1050        let required_bands = required_band_count(kind);
1051        if band_count < required_bands {
1052            return Err(GridError::Parse(format!(
1053                "{name}: GeoTIFF image {index} has {band_count} bands, needs at least {required_bands}"
1054            )));
1055        }
1056
1057        Ok(ImageMetadata {
1058            west_node_deg,
1059            north_node_deg,
1060            scale_lon_deg,
1061            scale_lat_deg,
1062            width,
1063            height,
1064            cell_count,
1065        })
1066    }
1067
1068    fn read_image(
1069        file: &GeoTiffFile,
1070        index: usize,
1071        kind: Kind,
1072        metadata: &ImageMetadata,
1073        name: &str,
1074    ) -> Result<Image, GridError> {
1075        let tiff = file.tiff();
1076        let ifd = tiff
1077            .ifd(index)
1078            .map_err(|err| GridError::Parse(format!("{name}: {err}")))?;
1079        let required_bands = required_band_count(kind);
1080        let mut bands = Vec::with_capacity(required_bands);
1081        // Horizontal grids need bands 0 (latitude) and 1 (longitude); vertical
1082        // grids need band 0. Accuracy bands, if present, are ignored.
1083        for band_index in 0..required_bands {
1084            let array = tiff
1085                .read_band_from_ifd::<f32>(ifd, band_index)
1086                .map_err(|err| GridError::Parse(format!("{name}: band {band_index}: {err}")))?;
1087            let values: Vec<f64> = array.iter().map(|&value| value as f64).collect();
1088            if values.len() != metadata.cell_count {
1089                return Err(GridError::Parse(format!(
1090                    "{name}: band {band_index} has {} samples, expected {}",
1091                    values.len(),
1092                    metadata.cell_count
1093                )));
1094            }
1095            bands.push(values);
1096        }
1097
1098        Ok(Image {
1099            west_node_deg: metadata.west_node_deg,
1100            north_node_deg: metadata.north_node_deg,
1101            scale_lon_deg: metadata.scale_lon_deg,
1102            scale_lat_deg: metadata.scale_lat_deg,
1103            width: metadata.width,
1104            height: metadata.height,
1105            bands,
1106        })
1107    }
1108
1109    fn required_band_count(kind: Kind) -> usize {
1110        match kind {
1111            Kind::Horizontal => 2,
1112            Kind::Vertical => 1,
1113        }
1114    }
1115
1116    /// Sample value at output node (x from west, y from south), flipping the
1117    /// row-major north-to-south raster into the south-to-north node order used
1118    /// by `Ntv2Grid`/`GtxGrid`.
1119    fn at(image: &Image, band: usize, x: usize, y: usize) -> f64 {
1120        let row = image.height - 1 - y;
1121        image.bands[band][row * image.width + x]
1122    }
1123
1124    fn build_gtx(image: &Image) -> GtxGrid {
1125        let width = image.width;
1126        let height = image.height;
1127        let mut offsets_meters = vec![0.0f64; width * height];
1128        for y in 0..height {
1129            for x in 0..width {
1130                offsets_meters[y * width + x] = at(image, 0, x, y);
1131            }
1132        }
1133        let west_degrees = image.west_node_deg;
1134        let south_degrees = image.north_node_deg - image.scale_lat_deg * (height - 1) as f64;
1135        GtxGrid {
1136            west_degrees,
1137            south_degrees,
1138            east_degrees: west_degrees + image.scale_lon_deg * (width - 1) as f64,
1139            north_degrees: image.north_node_deg,
1140            delta_lon_degrees: image.scale_lon_deg,
1141            delta_lat_degrees: image.scale_lat_deg,
1142            width,
1143            height,
1144            offsets_meters,
1145        }
1146    }
1147
1148    fn build_ntv2(images: &[Image], name: &str) -> Result<Ntv2GridSet, GridError> {
1149        let mut grids = Vec::with_capacity(images.len());
1150        for image in images {
1151            if image.bands.len() < 2 {
1152                return Err(GridError::Parse(format!(
1153                    "{name}: horizontal GeoTIFF grid needs latitude and longitude offset bands"
1154                )));
1155            }
1156            let width = image.width;
1157            let height = image.height;
1158            let mut lat_shift = vec![0.0f64; width * height];
1159            let mut lon_shift = vec![0.0f64; width * height];
1160            for y in 0..height {
1161                for x in 0..width {
1162                    let dest = y * width + x;
1163                    // Band 0: latitude offset (arc-sec, +north).
1164                    // Band 1: longitude offset (arc-sec, +east).
1165                    lat_shift[dest] = at(image, 0, x, y) * ARCSEC_TO_RAD;
1166                    lon_shift[dest] = at(image, 1, x, y) * ARCSEC_TO_RAD;
1167                }
1168            }
1169            let west = image.west_node_deg * DEG_TO_RAD;
1170            let north = image.north_node_deg * DEG_TO_RAD;
1171            let res_x = image.scale_lon_deg * DEG_TO_RAD;
1172            let res_y = image.scale_lat_deg * DEG_TO_RAD;
1173            let extent = GridExtent {
1174                west,
1175                south: north - res_y * (height - 1) as f64,
1176                east: west + res_x * (width - 1) as f64,
1177                north,
1178                res_x,
1179                res_y,
1180            };
1181            grids.push(Ntv2Grid {
1182                name: name.into(),
1183                extent,
1184                width,
1185                height,
1186                lat_shift,
1187                lon_shift,
1188                children: Vec::new(),
1189            });
1190        }
1191
1192        // Establish nesting: a grid's parent is the smallest other grid that
1193        // fully contains it. Grids without a parent are roots. PROJ stores
1194        // coarse parent grids before their finer nested children.
1195        let mut roots = Vec::new();
1196        for child in 0..grids.len() {
1197            let mut parent: Option<usize> = None;
1198            for candidate in 0..grids.len() {
1199                if candidate == child || !extent_contains(&grids[candidate], &grids[child]) {
1200                    continue;
1201                }
1202                match parent {
1203                    Some(current) if !extent_contains(&grids[current], &grids[candidate]) => {}
1204                    _ => parent = Some(candidate),
1205                }
1206            }
1207            match parent {
1208                Some(parent_index) => grids[parent_index].children.push(child),
1209                None => roots.push(child),
1210            }
1211        }
1212        if roots.is_empty() {
1213            return Err(GridError::Parse(format!(
1214                "{name}: horizontal GeoTIFF grid has no root subgrid"
1215            )));
1216        }
1217
1218        Ok(Ntv2GridSet { grids, roots })
1219    }
1220
1221    fn extent_contains(outer: &Ntv2Grid, inner: &Ntv2Grid) -> bool {
1222        let tol = (outer.extent.res_x + outer.extent.res_y) * 1e-9;
1223        outer.extent.west <= inner.extent.west + tol
1224            && outer.extent.east >= inner.extent.east - tol
1225            && outer.extent.south <= inner.extent.south + tol
1226            && outer.extent.north >= inner.extent.north - tol
1227            // A strictly larger cell is a coarser (parent) grid.
1228            && outer.extent.res_x > inner.extent.res_x * (1.0 + 1e-9)
1229    }
1230
1231    fn doubles(value: Option<&TagValue>) -> Option<Vec<f64>> {
1232        match value? {
1233            TagValue::Double(values) => Some(values.clone()),
1234            TagValue::Float(values) => Some(values.iter().map(|&v| v as f64).collect()),
1235            _ => None,
1236        }
1237    }
1238}
1239
1240fn parse_cached_grid_data(
1241    format: GridFormat,
1242    name: &str,
1243    bytes: &[u8],
1244) -> std::result::Result<CachedGridData, GridError> {
1245    Ok(CachedGridData {
1246        data: parse_grid_data(format, name, bytes)?,
1247        checksum: sha256_hex(bytes),
1248    })
1249}
1250
1251fn sha256_hex(bytes: &[u8]) -> String {
1252    use sha2::{Digest, Sha256};
1253
1254    let digest = Sha256::digest(bytes);
1255    let mut out = String::with_capacity(71);
1256    out.push_str("sha256:");
1257    for byte in digest {
1258        use std::fmt::Write as _;
1259        write!(&mut out, "{byte:02x}").expect("writing to string cannot fail");
1260    }
1261    out
1262}
1263
1264fn embedded_grid_resource(names: &[String]) -> Option<(&'static str, &'static [u8])> {
1265    for name in names {
1266        if name.eq_ignore_ascii_case("ntv2_0.gsb") {
1267            return Some(("ntv2_0.gsb", include_bytes!("../data/grids/ntv2_0.gsb")));
1268        }
1269    }
1270    None
1271}
1272
1273#[derive(Clone)]
1274struct Ntv2GridSet {
1275    grids: Vec<Ntv2Grid>,
1276    roots: Vec<usize>,
1277}
1278
1279impl Ntv2GridSet {
1280    fn parse(bytes: &[u8]) -> std::result::Result<Self, GridError> {
1281        if bytes.len() < NTV2_HEADER_LEN {
1282            return Err(GridError::Parse("NTv2 file too small".into()));
1283        }
1284        if bytes.len() > MAX_NTV2_GRID_BYTES {
1285            return Err(GridError::Parse(format!(
1286                "NTv2 grid exceeds maximum size of {MAX_NTV2_GRID_BYTES} bytes"
1287            )));
1288        }
1289
1290        let endian = if u32::from_le_bytes(bytes[8..12].try_into().expect("slice length checked"))
1291            == 11
1292        {
1293            Endian::Little
1294        } else if u32::from_be_bytes(bytes[8..12].try_into().expect("slice length checked")) == 11 {
1295            Endian::Big
1296        } else {
1297            return Err(GridError::Parse(
1298                "invalid NTv2 header endianness marker".into(),
1299            ));
1300        };
1301
1302        if &bytes[56..63] != b"SECONDS" {
1303            return Err(GridError::Parse(
1304                "only NTv2 GS_TYPE=SECONDS is supported".into(),
1305            ));
1306        }
1307
1308        let num_subfiles = read_u32(bytes, 40, endian)? as usize;
1309        if num_subfiles == 0 || num_subfiles > MAX_NTV2_SUBFILES {
1310            return Err(GridError::Parse(format!(
1311                "NTv2 subfile count {num_subfiles} exceeds limit {MAX_NTV2_SUBFILES}"
1312            )));
1313        }
1314
1315        let mut offset = NTV2_HEADER_LEN;
1316        let mut grids = Vec::with_capacity(num_subfiles);
1317        let mut name_to_index = HashMap::new();
1318        let mut parent_links: Vec<Option<String>> = Vec::with_capacity(num_subfiles);
1319        let mut total_cells = 0usize;
1320        let mut total_data_bytes = 0usize;
1321
1322        for _ in 0..num_subfiles {
1323            let header_end = offset
1324                .checked_add(NTV2_HEADER_LEN)
1325                .ok_or_else(|| GridError::Parse("NTv2 header offset overflow".into()))?;
1326            let header = bytes
1327                .get(offset..header_end)
1328                .ok_or_else(|| GridError::Parse("truncated NTv2 subfile header".into()))?;
1329            if &header[0..8] != b"SUB_NAME" {
1330                return Err(GridError::Parse("invalid NTv2 subfile header tag".into()));
1331            }
1332
1333            let name = parse_label(&header[8..16]);
1334            let parent = parse_label(&header[24..32]);
1335            let south = read_f64(header, 72, endian)? * PI / 180.0 / 3600.0;
1336            let north = read_f64(header, 88, endian)? * PI / 180.0 / 3600.0;
1337            let east = -read_f64(header, 104, endian)? * PI / 180.0 / 3600.0;
1338            let west = -read_f64(header, 120, endian)? * PI / 180.0 / 3600.0;
1339            let res_y = read_f64(header, 136, endian)? * PI / 180.0 / 3600.0;
1340            let res_x = read_f64(header, 152, endian)? * PI / 180.0 / 3600.0;
1341            let gs_count = read_u32(header, 168, endian)? as usize;
1342
1343            if !(west.is_finite()
1344                && east.is_finite()
1345                && south.is_finite()
1346                && north.is_finite()
1347                && res_x.is_finite()
1348                && res_y.is_finite()
1349                && west < east
1350                && south < north
1351                && res_x > 0.0
1352                && res_y > 0.0)
1353            {
1354                return Err(GridError::Parse(format!(
1355                    "invalid NTv2 georeferencing for subgrid {name}"
1356                )));
1357            }
1358
1359            let width = ntv2_axis_cell_count(east - west, res_x, "longitude", &name)?;
1360            let height = ntv2_axis_cell_count(north - south, res_y, "latitude", &name)?;
1361            let derived_cells = width
1362                .checked_mul(height)
1363                .ok_or_else(|| GridError::Parse("NTv2 cell count overflow".into()))?;
1364            if derived_cells > MAX_NTV2_CELLS_PER_SUBGRID {
1365                return Err(GridError::Parse(format!(
1366                    "NTv2 subgrid {name} has {derived_cells} cells, exceeding limit {MAX_NTV2_CELLS_PER_SUBGRID}"
1367                )));
1368            }
1369            if derived_cells != gs_count {
1370                return Err(GridError::Parse(format!(
1371                    "NTv2 subgrid {name} cell count mismatch: expected {} got {gs_count}",
1372                    derived_cells
1373                )));
1374            }
1375
1376            total_cells = total_cells
1377                .checked_add(gs_count)
1378                .ok_or_else(|| GridError::Parse("NTv2 total cell count overflow".into()))?;
1379            if total_cells > MAX_NTV2_TOTAL_CELLS {
1380                return Err(GridError::Parse(format!(
1381                    "NTv2 total cell count {total_cells} exceeds limit {MAX_NTV2_TOTAL_CELLS}"
1382                )));
1383            }
1384
1385            let data_len = gs_count
1386                .checked_mul(NTV2_RECORD_LEN)
1387                .ok_or_else(|| GridError::Parse("NTv2 data size overflow".into()))?;
1388            total_data_bytes = total_data_bytes
1389                .checked_add(data_len)
1390                .ok_or_else(|| GridError::Parse("NTv2 total data size overflow".into()))?;
1391            if total_data_bytes > MAX_NTV2_TOTAL_DATA_BYTES {
1392                return Err(GridError::Parse(format!(
1393                    "NTv2 data size {total_data_bytes} exceeds limit {MAX_NTV2_TOTAL_DATA_BYTES}"
1394                )));
1395            }
1396            let data_end = header_end
1397                .checked_add(data_len)
1398                .ok_or_else(|| GridError::Parse("NTv2 data offset overflow".into()))?;
1399            let data = bytes.get(header_end..data_end).ok_or_else(|| {
1400                GridError::Parse(format!("truncated NTv2 data for subgrid {name}"))
1401            })?;
1402
1403            let mut lat_shift = vec![0.0f64; gs_count];
1404            let mut lon_shift = vec![0.0f64; gs_count];
1405            for y in 0..height {
1406                for x in 0..width {
1407                    let source_x = width - 1 - x;
1408                    let record_offset = (y * width + source_x) * NTV2_RECORD_LEN;
1409                    let lat = read_f32(data, record_offset, endian)? as f64 * PI / 180.0 / 3600.0;
1410                    let lon =
1411                        -(read_f32(data, record_offset + 4, endian)? as f64) * PI / 180.0 / 3600.0;
1412                    if !(lat.is_finite() && lon.is_finite()) {
1413                        return Err(GridError::Parse(format!(
1414                            "non-finite NTv2 shift value in subgrid {name}"
1415                        )));
1416                    }
1417                    let dest = y * width + x;
1418                    lat_shift[dest] = lat;
1419                    lon_shift[dest] = lon;
1420                }
1421            }
1422
1423            let index = grids.len();
1424            name_to_index.insert(name.clone(), index);
1425            parent_links.push(
1426                if parent.eq_ignore_ascii_case("none") || parent.is_empty() {
1427                    None
1428                } else {
1429                    Some(parent)
1430                },
1431            );
1432            grids.push(Ntv2Grid {
1433                name,
1434                extent: GridExtent {
1435                    west,
1436                    south,
1437                    east,
1438                    north,
1439                    res_x,
1440                    res_y,
1441                },
1442                width,
1443                height,
1444                lat_shift,
1445                lon_shift,
1446                children: Vec::new(),
1447            });
1448            offset = data_end;
1449        }
1450
1451        let mut roots = Vec::new();
1452        for (idx, parent) in parent_links.into_iter().enumerate() {
1453            if let Some(parent_name) = parent {
1454                let Some(parent_idx) = name_to_index.get(&parent_name).copied() else {
1455                    return Err(GridError::Parse(format!(
1456                        "missing NTv2 parent subgrid {parent_name} for {}",
1457                        grids[idx].name
1458                    )));
1459                };
1460                grids[parent_idx].children.push(idx);
1461            } else {
1462                roots.push(idx);
1463            }
1464        }
1465
1466        Ok(Self { grids, roots })
1467    }
1468
1469    fn sample(
1470        &self,
1471        lon_radians: f64,
1472        lat_radians: f64,
1473    ) -> std::result::Result<GridSample, GridError> {
1474        let (grid_idx, local_lon, local_lat) = self.grid_at(lon_radians, lat_radians)?;
1475        let (lon_shift, lat_shift) = interpolate(&self.grids[grid_idx], local_lon, local_lat)?;
1476        Ok(GridSample {
1477            lon_shift_radians: lon_shift,
1478            lat_shift_radians: lat_shift,
1479        })
1480    }
1481
1482    fn apply(
1483        &self,
1484        lon_radians: f64,
1485        lat_radians: f64,
1486        direction: GridShiftDirection,
1487    ) -> std::result::Result<(f64, f64), GridError> {
1488        match direction {
1489            GridShiftDirection::Forward => {
1490                let shift = self.sample(lon_radians, lat_radians)?;
1491                Ok((
1492                    lon_radians + shift.lon_shift_radians,
1493                    lat_radians + shift.lat_shift_radians,
1494                ))
1495            }
1496            GridShiftDirection::Reverse => self.apply_inverse(lon_radians, lat_radians),
1497        }
1498    }
1499
1500    fn apply_inverse(
1501        &self,
1502        lon_radians: f64,
1503        lat_radians: f64,
1504    ) -> std::result::Result<(f64, f64), GridError> {
1505        const MAX_ITERATIONS: usize = 10;
1506        const TOLERANCE: f64 = 1e-12;
1507
1508        let mut estimate_lon = lon_radians;
1509        let mut estimate_lat = lat_radians;
1510
1511        for _ in 0..MAX_ITERATIONS {
1512            let shift = self.sample(estimate_lon, estimate_lat)?;
1513            let next_lon = lon_radians - shift.lon_shift_radians;
1514            let next_lat = lat_radians - shift.lat_shift_radians;
1515            let diff_lon = next_lon - estimate_lon;
1516            let diff_lat = next_lat - estimate_lat;
1517            estimate_lon = next_lon;
1518            estimate_lat = next_lat;
1519            if diff_lon * diff_lon + diff_lat * diff_lat <= TOLERANCE * TOLERANCE {
1520                return Ok((estimate_lon, estimate_lat));
1521            }
1522        }
1523
1524        // Matches C PROJ's nad_cvt, which fails the point after MAX_TRY
1525        // instead of returning a non-converged estimate. A wandering fixed
1526        // point means the pull-back left reliable coverage, so surface it as
1527        // a coverage error and let grid fallbacks handle it.
1528        Err(GridError::OutsideCoverage(format!(
1529            "NTv2 inverse shift did not converge at longitude {:.8} latitude {:.8}",
1530            lon_radians.to_degrees(),
1531            lat_radians.to_degrees()
1532        )))
1533    }
1534
1535    fn grid_at(
1536        &self,
1537        lon_radians: f64,
1538        lat_radians: f64,
1539    ) -> std::result::Result<(usize, f64, f64), GridError> {
1540        for &root in &self.roots {
1541            let lon = self.grids[root].extent.normalize_lon(lon_radians);
1542            if self.grids[root].extent.contains(lon, lat_radians) {
1543                let idx = self.deepest_child(root, lon, lat_radians);
1544                let extent = &self.grids[idx].extent;
1545                return Ok((idx, lon - extent.west, lat_radians - extent.south));
1546            }
1547        }
1548        Err(GridError::OutsideCoverage(format!(
1549            "longitude {:.8} latitude {:.8}",
1550            lon_radians.to_degrees(),
1551            lat_radians.to_degrees()
1552        )))
1553    }
1554
1555    fn deepest_child(&self, index: usize, lon_radians: f64, lat_radians: f64) -> usize {
1556        for &child in &self.grids[index].children {
1557            if self.grids[child].extent.contains(lon_radians, lat_radians) {
1558                return self.deepest_child(child, lon_radians, lat_radians);
1559            }
1560        }
1561        index
1562    }
1563}
1564
1565fn ntv2_axis_cell_count(
1566    span: f64,
1567    resolution: f64,
1568    axis: &str,
1569    name: &str,
1570) -> std::result::Result<usize, GridError> {
1571    let intervals = span / resolution;
1572    if !intervals.is_finite() || intervals < 0.0 {
1573        return Err(GridError::Parse(format!(
1574            "invalid NTv2 {axis} spacing for subgrid {name}"
1575        )));
1576    }
1577
1578    let rounded_intervals = (intervals + 0.5).floor();
1579    if !rounded_intervals.is_finite() || rounded_intervals > (MAX_NTV2_CELLS_PER_SUBGRID - 1) as f64
1580    {
1581        return Err(GridError::Parse(format!(
1582            "NTv2 subgrid {name} {axis} cell count exceeds limit {MAX_NTV2_CELLS_PER_SUBGRID}"
1583        )));
1584    }
1585
1586    let count = rounded_intervals as usize + 1;
1587    if count < 2 {
1588        return Err(GridError::Parse(format!(
1589            "NTv2 subgrid {name} has fewer than two {axis} cells"
1590        )));
1591    }
1592    Ok(count)
1593}
1594
1595#[derive(Clone)]
1596struct Ntv2Grid {
1597    name: String,
1598    extent: GridExtent,
1599    width: usize,
1600    height: usize,
1601    lat_shift: Vec<f64>,
1602    lon_shift: Vec<f64>,
1603    children: Vec<usize>,
1604}
1605
1606#[derive(Clone, Copy)]
1607struct GridExtent {
1608    west: f64,
1609    south: f64,
1610    east: f64,
1611    north: f64,
1612    res_x: f64,
1613    res_y: f64,
1614}
1615
1616impl GridExtent {
1617    fn contains(&self, lon_radians: f64, lat_radians: f64) -> bool {
1618        let epsilon = (self.res_x + self.res_y) * 1e-10;
1619        lon_radians >= self.west - epsilon
1620            && lon_radians <= self.east + epsilon
1621            && lat_radians >= self.south - epsilon
1622            && lat_radians <= self.north + epsilon
1623    }
1624
1625    /// Wrap a longitude into this extent's 360° branch, mirroring the GTX
1626    /// grid behavior; longitudes already inside the extent (within its
1627    /// epsilon tolerance) are returned unchanged.
1628    fn normalize_lon(&self, lon_radians: f64) -> f64 {
1629        if self.contains(lon_radians, self.south) {
1630            return lon_radians;
1631        }
1632
1633        self.west + (lon_radians - self.west).rem_euclid(std::f64::consts::TAU)
1634    }
1635}
1636
1637fn interpolate(
1638    grid: &Ntv2Grid,
1639    local_lon: f64,
1640    local_lat: f64,
1641) -> std::result::Result<(f64, f64), GridError> {
1642    let lam = local_lon / grid.extent.res_x;
1643    let phi = local_lat / grid.extent.res_y;
1644    let mut x = lam.floor() as isize;
1645    let mut y = phi.floor() as isize;
1646    let mut fx = lam - x as f64;
1647    let mut fy = phi - y as f64;
1648
1649    if x < 0 {
1650        if x == -1 && fx > 1.0 - 1e-9 {
1651            x = 0;
1652            fx = 0.0;
1653        } else {
1654            return Err(GridError::OutsideCoverage(grid.name.clone()));
1655        }
1656    }
1657    if y < 0 {
1658        if y == -1 && fy > 1.0 - 1e-9 {
1659            y = 0;
1660            fy = 0.0;
1661        } else {
1662            return Err(GridError::OutsideCoverage(grid.name.clone()));
1663        }
1664    }
1665    if x as usize + 1 >= grid.width {
1666        if x as usize + 1 == grid.width && fx < 1e-9 {
1667            x -= 1;
1668            fx = 1.0;
1669        } else {
1670            return Err(GridError::OutsideCoverage(grid.name.clone()));
1671        }
1672    }
1673    if y as usize + 1 >= grid.height {
1674        if y as usize + 1 == grid.height && fy < 1e-9 {
1675            y -= 1;
1676            fy = 1.0;
1677        } else {
1678            return Err(GridError::OutsideCoverage(grid.name.clone()));
1679        }
1680    }
1681
1682    let idx = |xx: usize, yy: usize| yy * grid.width + xx;
1683    let x0 = x as usize;
1684    let y0 = y as usize;
1685    let x1 = x0 + 1;
1686    let y1 = y0 + 1;
1687
1688    let m00 = (1.0 - fx) * (1.0 - fy);
1689    let m10 = fx * (1.0 - fy);
1690    let m01 = (1.0 - fx) * fy;
1691    let m11 = fx * fy;
1692
1693    let lon = m00 * grid.lon_shift[idx(x0, y0)]
1694        + m10 * grid.lon_shift[idx(x1, y0)]
1695        + m01 * grid.lon_shift[idx(x0, y1)]
1696        + m11 * grid.lon_shift[idx(x1, y1)];
1697    let lat = m00 * grid.lat_shift[idx(x0, y0)]
1698        + m10 * grid.lat_shift[idx(x1, y0)]
1699        + m01 * grid.lat_shift[idx(x0, y1)]
1700        + m11 * grid.lat_shift[idx(x1, y1)];
1701
1702    Ok((lon, lat))
1703}
1704
1705#[derive(Clone)]
1706struct GtxGrid {
1707    west_degrees: f64,
1708    south_degrees: f64,
1709    east_degrees: f64,
1710    north_degrees: f64,
1711    delta_lon_degrees: f64,
1712    delta_lat_degrees: f64,
1713    width: usize,
1714    height: usize,
1715    offsets_meters: Vec<f64>,
1716}
1717
1718impl GtxGrid {
1719    fn parse(bytes: &[u8]) -> std::result::Result<Self, GridError> {
1720        if bytes.len() < GTX_HEADER_LEN {
1721            return Err(GridError::Parse("GTX file too small".into()));
1722        }
1723        if bytes.len() > MAX_GTX_GRID_BYTES {
1724            return Err(GridError::Parse(format!(
1725                "GTX grid exceeds maximum size of {MAX_GTX_GRID_BYTES} bytes"
1726            )));
1727        }
1728
1729        let south_degrees = read_f64(bytes, 0, Endian::Big)?;
1730        let west_degrees = read_f64(bytes, 8, Endian::Big)?;
1731        let delta_lat_degrees = read_f64(bytes, 16, Endian::Big)?;
1732        let delta_lon_degrees = read_f64(bytes, 24, Endian::Big)?;
1733        let height_i32 = read_i32(bytes, 32, Endian::Big)?;
1734        let width_i32 = read_i32(bytes, 36, Endian::Big)?;
1735
1736        if !(west_degrees.is_finite()
1737            && south_degrees.is_finite()
1738            && delta_lon_degrees.is_finite()
1739            && delta_lat_degrees.is_finite()
1740            && delta_lon_degrees > 0.0
1741            && delta_lat_degrees > 0.0
1742            && width_i32 >= 2
1743            && height_i32 >= 2)
1744        {
1745            return Err(GridError::Parse("invalid GTX georeferencing".into()));
1746        }
1747        let height = height_i32 as usize;
1748        let width = width_i32 as usize;
1749
1750        let count = width
1751            .checked_mul(height)
1752            .ok_or_else(|| GridError::Parse("GTX data size overflow".into()))?;
1753        if count > MAX_GTX_CELLS {
1754            return Err(GridError::Parse(format!(
1755                "GTX cell count {count} exceeds limit {MAX_GTX_CELLS}"
1756            )));
1757        }
1758        let data_len = count
1759            .checked_mul(GTX_RECORD_LEN)
1760            .ok_or_else(|| GridError::Parse("GTX data size overflow".into()))?;
1761        let expected_len = GTX_HEADER_LEN
1762            .checked_add(data_len)
1763            .ok_or_else(|| GridError::Parse("GTX data size overflow".into()))?;
1764        if expected_len > MAX_GTX_GRID_BYTES {
1765            return Err(GridError::Parse(format!(
1766                "GTX data size {expected_len} exceeds limit {MAX_GTX_GRID_BYTES}"
1767            )));
1768        }
1769        if bytes.len() < expected_len {
1770            return Err(GridError::Parse("truncated GTX data".into()));
1771        }
1772
1773        let mut offsets_meters = Vec::with_capacity(count);
1774        for index in 0..count {
1775            let value =
1776                read_f32(bytes, GTX_HEADER_LEN + index * GTX_RECORD_LEN, Endian::Big)? as f64;
1777            if (value + 88.8888).abs() <= 1e-4 {
1778                offsets_meters.push(f64::NAN);
1779            } else {
1780                offsets_meters.push(value);
1781            }
1782        }
1783
1784        let east_degrees = west_degrees + delta_lon_degrees * (width - 1) as f64;
1785        let north_degrees = south_degrees + delta_lat_degrees * (height - 1) as f64;
1786
1787        Ok(Self {
1788            west_degrees,
1789            south_degrees,
1790            east_degrees,
1791            north_degrees,
1792            delta_lon_degrees,
1793            delta_lat_degrees,
1794            width,
1795            height,
1796            offsets_meters,
1797        })
1798    }
1799
1800    fn sample(
1801        &self,
1802        lon_radians: f64,
1803        lat_radians: f64,
1804    ) -> std::result::Result<VerticalGridSample, GridError> {
1805        let raw_lon_degrees = lon_radians.to_degrees();
1806        let lat_degrees = lat_radians.to_degrees();
1807
1808        if !(raw_lon_degrees.is_finite() && lat_degrees.is_finite()) {
1809            return Err(GridError::OutsideCoverage(format!(
1810                "non-finite longitude {:.8} latitude {:.8}",
1811                raw_lon_degrees, lat_degrees
1812            )));
1813        }
1814
1815        let lon_degrees = self.normalize_lon_degrees(raw_lon_degrees);
1816
1817        if !self.contains(lon_degrees, lat_degrees) {
1818            return Err(GridError::OutsideCoverage(format!(
1819                "longitude {:.8} latitude {:.8}",
1820                raw_lon_degrees, lat_degrees
1821            )));
1822        }
1823
1824        let lam = (lon_degrees - self.west_degrees) / self.delta_lon_degrees;
1825        let phi = (lat_degrees - self.south_degrees) / self.delta_lat_degrees;
1826        let mut x = lam.floor() as isize;
1827        let mut y = phi.floor() as isize;
1828        let mut fx = lam - x as f64;
1829        let mut fy = phi - y as f64;
1830
1831        if x < 0 {
1832            if x == -1 && fx > 1.0 - 1e-9 {
1833                x = 0;
1834                fx = 0.0;
1835            } else {
1836                return Err(GridError::OutsideCoverage("GTX negative grid index".into()));
1837            }
1838        }
1839        if y < 0 {
1840            if y == -1 && fy > 1.0 - 1e-9 {
1841                y = 0;
1842                fy = 0.0;
1843            } else {
1844                return Err(GridError::OutsideCoverage("GTX negative grid index".into()));
1845            }
1846        }
1847        if x as usize + 1 >= self.width {
1848            if x as usize + 1 == self.width && fx < 1e-9 {
1849                x -= 1;
1850                fx = 1.0;
1851            } else {
1852                return Err(GridError::OutsideCoverage("GTX longitude edge".into()));
1853            }
1854        }
1855        if y as usize + 1 >= self.height {
1856            if y as usize + 1 == self.height && fy < 1e-9 {
1857                y -= 1;
1858                fy = 1.0;
1859            } else {
1860                return Err(GridError::OutsideCoverage("GTX latitude edge".into()));
1861            }
1862        }
1863
1864        let x0 = x as usize;
1865        let y0 = y as usize;
1866        let x1 = x0 + 1;
1867        let y1 = y0 + 1;
1868        let idx = |xx: usize, yy: usize| yy * self.width + xx;
1869        let z00 = self.offsets_meters[idx(x0, y0)];
1870        let z10 = self.offsets_meters[idx(x1, y0)];
1871        let z01 = self.offsets_meters[idx(x0, y1)];
1872        let z11 = self.offsets_meters[idx(x1, y1)];
1873
1874        if !(z00.is_finite() && z10.is_finite() && z01.is_finite() && z11.is_finite()) {
1875            return Err(GridError::OutsideCoverage(
1876                "GTX interpolation touches a null cell".into(),
1877            ));
1878        }
1879
1880        let m00 = (1.0 - fx) * (1.0 - fy);
1881        let m10 = fx * (1.0 - fy);
1882        let m01 = (1.0 - fx) * fy;
1883        let m11 = fx * fy;
1884        Ok(VerticalGridSample {
1885            offset_meters: m00 * z00 + m10 * z10 + m01 * z01 + m11 * z11,
1886        })
1887    }
1888
1889    fn contains(&self, lon_degrees: f64, lat_degrees: f64) -> bool {
1890        let epsilon = (self.delta_lon_degrees + self.delta_lat_degrees) * 1e-10;
1891        lon_degrees >= self.west_degrees - epsilon
1892            && lon_degrees <= self.east_degrees + epsilon
1893            && lat_degrees >= self.south_degrees - epsilon
1894            && lat_degrees <= self.north_degrees + epsilon
1895    }
1896
1897    fn normalize_lon_degrees(&self, lon_degrees: f64) -> f64 {
1898        if self.contains(lon_degrees, self.south_degrees) {
1899            return lon_degrees;
1900        }
1901
1902        self.west_degrees + (lon_degrees - self.west_degrees).rem_euclid(360.0)
1903    }
1904}
1905
1906#[derive(Clone, Copy)]
1907enum Endian {
1908    Little,
1909    Big,
1910}
1911
1912fn parse_label(bytes: &[u8]) -> String {
1913    let end = bytes
1914        .iter()
1915        .position(|byte| *byte == 0)
1916        .unwrap_or(bytes.len());
1917    String::from_utf8_lossy(&bytes[..end]).trim().to_string()
1918}
1919
1920fn read_u32(bytes: &[u8], offset: usize, endian: Endian) -> std::result::Result<u32, GridError> {
1921    let end = offset
1922        .checked_add(4)
1923        .ok_or_else(|| GridError::Parse("integer offset overflow".into()))?;
1924    let slice = bytes
1925        .get(offset..end)
1926        .ok_or_else(|| GridError::Parse("truncated integer".into()))?;
1927    Ok(match endian {
1928        Endian::Little => u32::from_le_bytes(slice.try_into().expect("slice length checked")),
1929        Endian::Big => u32::from_be_bytes(slice.try_into().expect("slice length checked")),
1930    })
1931}
1932
1933fn read_i32(bytes: &[u8], offset: usize, endian: Endian) -> std::result::Result<i32, GridError> {
1934    let end = offset
1935        .checked_add(4)
1936        .ok_or_else(|| GridError::Parse("integer offset overflow".into()))?;
1937    let slice = bytes
1938        .get(offset..end)
1939        .ok_or_else(|| GridError::Parse("truncated integer".into()))?;
1940    Ok(match endian {
1941        Endian::Little => i32::from_le_bytes(slice.try_into().expect("slice length checked")),
1942        Endian::Big => i32::from_be_bytes(slice.try_into().expect("slice length checked")),
1943    })
1944}
1945
1946fn read_f64(bytes: &[u8], offset: usize, endian: Endian) -> std::result::Result<f64, GridError> {
1947    let end = offset
1948        .checked_add(8)
1949        .ok_or_else(|| GridError::Parse("float64 offset overflow".into()))?;
1950    let slice = bytes
1951        .get(offset..end)
1952        .ok_or_else(|| GridError::Parse("truncated float64".into()))?;
1953    Ok(match endian {
1954        Endian::Little => f64::from_le_bytes(slice.try_into().expect("slice length checked")),
1955        Endian::Big => f64::from_be_bytes(slice.try_into().expect("slice length checked")),
1956    })
1957}
1958
1959fn read_f32(bytes: &[u8], offset: usize, endian: Endian) -> std::result::Result<f32, GridError> {
1960    let end = offset
1961        .checked_add(4)
1962        .ok_or_else(|| GridError::Parse("float32 offset overflow".into()))?;
1963    let slice = bytes
1964        .get(offset..end)
1965        .ok_or_else(|| GridError::Parse("truncated float32".into()))?;
1966    Ok(match endian {
1967        Endian::Little => f32::from_le_bytes(slice.try_into().expect("slice length checked")),
1968        Endian::Big => f32::from_be_bytes(slice.try_into().expect("slice length checked")),
1969    })
1970}
1971
1972#[cfg(test)]
1973mod tests {
1974    use super::*;
1975    use proptest::prelude::*;
1976    use std::sync::atomic::{AtomicUsize, Ordering};
1977    use std::sync::Barrier;
1978    use std::time::Duration;
1979
1980    #[test]
1981    fn embedded_ntv2_grid_samples_known_point() {
1982        let provider = EmbeddedGridProvider;
1983        let definition = GridDefinition {
1984            id: GridId(1),
1985            name: "ntv2_0.gsb".into(),
1986            format: GridFormat::Ntv2,
1987            interpolation: GridInterpolation::Bilinear,
1988            area_of_use: None,
1989            resource_names: SmallVec::from_vec(vec!["ntv2_0.gsb".into()]),
1990        };
1991        let handle = provider.load(&definition).unwrap().expect("embedded grid");
1992        let (lon, lat) = handle
1993            .apply(
1994                (-80.5041667f64).to_radians(),
1995                44.5458333f64.to_radians(),
1996                GridShiftDirection::Forward,
1997            )
1998            .unwrap();
1999        assert!(
2000            (lon.to_degrees() - (-80.50401615833)).abs() < 1e-6,
2001            "lon={lon}"
2002        );
2003        assert!((lat.to_degrees() - 44.5458827236).abs() < 3e-6, "lat={lat}");
2004    }
2005
2006    #[test]
2007    fn embedded_provider_reuses_parsed_grid_data() {
2008        let provider = EmbeddedGridProvider;
2009        let definition = test_grid_definition();
2010
2011        let first = provider.load(&definition).unwrap().expect("embedded grid");
2012        let mut renamed = definition.clone();
2013        renamed.name = "renamed ntv2 grid".into();
2014        let second = provider.load(&renamed).unwrap().expect("embedded grid");
2015
2016        assert!(Arc::ptr_eq(&first.data, &second.data));
2017        assert_eq!(second.definition().name, "renamed ntv2 grid");
2018    }
2019
2020    #[test]
2021    fn grid_handle_reports_sha256_checksum() {
2022        let provider = EmbeddedGridProvider;
2023        let handle = provider
2024            .load(&test_grid_definition())
2025            .unwrap()
2026            .expect("embedded grid");
2027
2028        assert!(handle.checksum().starts_with("sha256:"));
2029        assert_eq!(handle.checksum().len(), 71);
2030
2031        // FIPS 180-4 / NIST CAVP short-message vectors guard the digest
2032        // dependency itself.
2033        assert_eq!(
2034            sha256_hex(b"abc"),
2035            "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
2036        );
2037        assert_eq!(
2038            sha256_hex(b""),
2039            "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
2040        );
2041        assert_eq!(
2042            sha256_hex(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
2043            "sha256:248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
2044        );
2045    }
2046
2047    struct SingleFlightTrackingProvider {
2048        data_cache: GridDataCache,
2049        parse_calls: Arc<AtomicUsize>,
2050        bytes: Vec<u8>,
2051    }
2052
2053    impl GridProvider for SingleFlightTrackingProvider {
2054        fn definition(
2055            &self,
2056            grid: &GridDefinition,
2057        ) -> std::result::Result<Option<GridDefinition>, GridError> {
2058            Ok(Some(grid.clone()))
2059        }
2060
2061        fn load(
2062            &self,
2063            grid: &GridDefinition,
2064        ) -> std::result::Result<Option<GridHandle>, GridError> {
2065            let key = GridDataCacheKey::new(grid.format, "single-flight-test-grid");
2066            let data = cached_grid_data(&self.data_cache, key, || {
2067                self.parse_calls.fetch_add(1, Ordering::SeqCst);
2068                std::thread::sleep(Duration::from_millis(25));
2069                parse_cached_grid_data(grid.format, &grid.name, &self.bytes)
2070            })?;
2071
2072            Ok(Some(GridHandle {
2073                definition: grid.clone(),
2074                data,
2075            }))
2076        }
2077    }
2078
2079    #[test]
2080    fn cached_grid_data_single_flights_concurrent_loads() {
2081        const THREADS: usize = 12;
2082
2083        let parse_calls = Arc::new(AtomicUsize::new(0));
2084        let provider = Arc::new(SingleFlightTrackingProvider {
2085            data_cache: Mutex::new(HashMap::new()),
2086            parse_calls: Arc::clone(&parse_calls),
2087            bytes: test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]),
2088        });
2089        let definition = GridDefinition {
2090            id: GridId(9_999),
2091            name: "single-flight-test.gtx".into(),
2092            format: GridFormat::Gtx,
2093            interpolation: GridInterpolation::Bilinear,
2094            area_of_use: None,
2095            resource_names: SmallVec::from_vec(vec!["single-flight-test.gtx".into()]),
2096        };
2097        let barrier = Arc::new(Barrier::new(THREADS));
2098
2099        let handles = std::thread::scope(|scope| {
2100            let mut joins = Vec::new();
2101            for _ in 0..THREADS {
2102                let provider = Arc::clone(&provider);
2103                let definition = definition.clone();
2104                let barrier = Arc::clone(&barrier);
2105                joins.push(scope.spawn(move || {
2106                    barrier.wait();
2107                    provider.load(&definition).unwrap().unwrap()
2108                }));
2109            }
2110
2111            joins
2112                .into_iter()
2113                .map(|join| join.join().unwrap())
2114                .collect::<Vec<_>>()
2115        });
2116
2117        assert_eq!(parse_calls.load(Ordering::SeqCst), 1);
2118        for handle in &handles[1..] {
2119            assert!(Arc::ptr_eq(&handles[0].data, &handle.data));
2120            assert_eq!(handles[0].checksum(), handle.checksum());
2121        }
2122    }
2123
2124    struct TrackingGridProvider {
2125        override_definition: bool,
2126        definition_calls: Arc<AtomicUsize>,
2127        load_calls: Arc<AtomicUsize>,
2128    }
2129
2130    impl GridProvider for TrackingGridProvider {
2131        fn definition(
2132            &self,
2133            grid: &GridDefinition,
2134        ) -> std::result::Result<Option<GridDefinition>, GridError> {
2135            self.definition_calls.fetch_add(1, Ordering::SeqCst);
2136            if self.override_definition {
2137                let mut overridden = grid.clone();
2138                overridden.name = "custom override".into();
2139                Ok(Some(overridden))
2140            } else {
2141                Ok(None)
2142            }
2143        }
2144
2145        fn load(
2146            &self,
2147            grid: &GridDefinition,
2148        ) -> std::result::Result<Option<GridHandle>, GridError> {
2149            self.load_calls.fetch_add(1, Ordering::SeqCst);
2150            EmbeddedGridProvider.load(grid)
2151        }
2152    }
2153
2154    fn test_grid_definition() -> GridDefinition {
2155        GridDefinition {
2156            id: GridId(1),
2157            name: "ntv2_0.gsb".into(),
2158            format: GridFormat::Ntv2,
2159            interpolation: GridInterpolation::Bilinear,
2160            area_of_use: None,
2161            resource_names: SmallVec::from_vec(vec!["ntv2_0.gsb".into()]),
2162        }
2163    }
2164
2165    fn write_ntv2_global_header(header: &mut [u8], num_subfiles: u32) {
2166        header[8..12].copy_from_slice(&11u32.to_le_bytes());
2167        header[40..44].copy_from_slice(&num_subfiles.to_le_bytes());
2168        header[56..63].copy_from_slice(b"SECONDS");
2169    }
2170
2171    fn write_ntv2_label(header: &mut [u8], offset: usize, value: &str) {
2172        header[offset..offset + 8].fill(b' ');
2173        let bytes = value.as_bytes();
2174        let len = bytes.len().min(8);
2175        header[offset..offset + len].copy_from_slice(&bytes[..len]);
2176    }
2177
2178    fn write_ntv2_f64(header: &mut [u8], offset: usize, value: f64) {
2179        header[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
2180    }
2181
2182    fn write_ntv2_f64_bits(header: &mut [u8], offset: usize, bits: u64) {
2183        header[offset..offset + 8].copy_from_slice(&bits.to_le_bytes());
2184    }
2185
2186    fn write_ntv2_f32(bytes: &mut [u8], offset: usize, value: f32) {
2187        bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
2188    }
2189
2190    fn write_ntv2_u32(header: &mut [u8], offset: usize, value: u32) {
2191        header[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
2192    }
2193
2194    fn minimal_ntv2_bytes() -> Vec<u8> {
2195        let mut bytes = vec![0u8; NTV2_HEADER_LEN * 2 + 4 * NTV2_RECORD_LEN];
2196        write_ntv2_global_header(&mut bytes[..NTV2_HEADER_LEN], 1);
2197
2198        let header = &mut bytes[NTV2_HEADER_LEN..NTV2_HEADER_LEN * 2];
2199        header[0..8].copy_from_slice(b"SUB_NAME");
2200        write_ntv2_label(header, 8, "TEST");
2201        write_ntv2_label(header, 24, "NONE");
2202        write_ntv2_f64(header, 72, 0.0);
2203        write_ntv2_f64(header, 88, 3600.0);
2204        write_ntv2_f64(header, 104, 0.0);
2205        write_ntv2_f64(header, 120, 3600.0);
2206        write_ntv2_f64(header, 136, 3600.0);
2207        write_ntv2_f64(header, 152, 3600.0);
2208        write_ntv2_u32(header, 168, 4);
2209
2210        bytes
2211    }
2212
2213    /// Write one NTv2 subfile (header + constant-shift data) at `offset`.
2214    ///
2215    /// Extents are in degrees; the file stores positive-west arcseconds.
2216    /// Every node gets the same latitude shift (`lat_shift_arcsec`) and a zero
2217    /// longitude shift, so subgrid selection is observable through the value.
2218    #[allow(clippy::too_many_arguments)]
2219    fn write_ntv2_subfile(
2220        bytes: &mut [u8],
2221        offset: usize,
2222        name: &str,
2223        parent: &str,
2224        west_deg: f64,
2225        east_deg: f64,
2226        south_deg: f64,
2227        north_deg: f64,
2228        res_deg: f64,
2229        lat_shift_arcsec: f32,
2230    ) -> usize {
2231        let nodes_x = ((east_deg - west_deg) / res_deg).round() as usize + 1;
2232        let nodes_y = ((north_deg - south_deg) / res_deg).round() as usize + 1;
2233        let gs_count = nodes_x * nodes_y;
2234
2235        let header = &mut bytes[offset..offset + NTV2_HEADER_LEN];
2236        header[0..8].copy_from_slice(b"SUB_NAME");
2237        write_ntv2_label(header, 8, name);
2238        write_ntv2_label(header, 24, parent);
2239        write_ntv2_f64(header, 72, south_deg * 3600.0);
2240        write_ntv2_f64(header, 88, north_deg * 3600.0);
2241        write_ntv2_f64(header, 104, -east_deg * 3600.0);
2242        write_ntv2_f64(header, 120, -west_deg * 3600.0);
2243        write_ntv2_f64(header, 136, res_deg * 3600.0);
2244        write_ntv2_f64(header, 152, res_deg * 3600.0);
2245        write_ntv2_u32(header, 168, gs_count as u32);
2246
2247        let data_start = offset + NTV2_HEADER_LEN;
2248        for record in 0..gs_count {
2249            write_ntv2_f32(
2250                bytes,
2251                data_start + record * NTV2_RECORD_LEN,
2252                lat_shift_arcsec,
2253            );
2254        }
2255        data_start + gs_count * NTV2_RECORD_LEN
2256    }
2257
2258    /// A three-level NTv2 hierarchy with distinct constant latitude shifts:
2259    /// root AA (1″) ⊃ child BB (2″) ⊃ grandchild CC (3″).
2260    fn nested_ntv2_bytes() -> Vec<u8> {
2261        let mut bytes = vec![0u8; NTV2_HEADER_LEN * 4 + 3 * 25 * NTV2_RECORD_LEN];
2262        write_ntv2_global_header(&mut bytes[..NTV2_HEADER_LEN], 3);
2263
2264        let mut offset = NTV2_HEADER_LEN;
2265        offset = write_ntv2_subfile(
2266            &mut bytes, offset, "AA", "NONE", -4.0, 0.0, 0.0, 4.0, 1.0, 1.0,
2267        );
2268        offset = write_ntv2_subfile(
2269            &mut bytes, offset, "BB", "AA", -3.0, -1.0, 1.0, 3.0, 0.5, 2.0,
2270        );
2271        offset = write_ntv2_subfile(
2272            &mut bytes, offset, "CC", "BB", -2.5, -1.5, 1.5, 2.5, 0.25, 3.0,
2273        );
2274        assert_eq!(offset, bytes.len());
2275
2276        bytes
2277    }
2278
2279    #[test]
2280    fn ntv2_selects_deepest_nested_subgrid() {
2281        let set = Ntv2GridSet::parse(&nested_ntv2_bytes()).unwrap();
2282        let arcsec = PI / 180.0 / 3600.0;
2283
2284        let cases = [
2285            (-2.0, 2.0, 3.0, "inside grandchild CC"),
2286            (-2.8, 1.2, 2.0, "inside child BB, outside CC"),
2287            (-0.5, 0.5, 1.0, "inside root AA only"),
2288        ];
2289        for (lon_deg, lat_deg, expected_arcsec, label) in cases {
2290            let sample = set
2291                .sample(f64::to_radians(lon_deg), f64::to_radians(lat_deg))
2292                .unwrap_or_else(|e| panic!("{label}: {e}"));
2293            assert!(
2294                (sample.lat_shift_radians - expected_arcsec * arcsec).abs() < 1e-12,
2295                "{label}: got {} arcsec",
2296                sample.lat_shift_radians / arcsec
2297            );
2298        }
2299    }
2300
2301    #[test]
2302    fn ntv2_wraps_out_of_branch_longitude() {
2303        let set = Ntv2GridSet::parse(&nested_ntv2_bytes()).unwrap();
2304
2305        // 358°E is the same meridian as -2°E; GTX grids already wrap this way.
2306        let in_branch = set
2307            .sample(f64::to_radians(-2.0), f64::to_radians(2.0))
2308            .unwrap();
2309        let wrapped = set
2310            .sample(f64::to_radians(358.0), f64::to_radians(2.0))
2311            .expect("out-of-branch longitude should resolve to the same grid cell");
2312        assert!(
2313            (wrapped.lat_shift_radians - in_branch.lat_shift_radians).abs() < 1e-15
2314                && (wrapped.lon_shift_radians - in_branch.lon_shift_radians).abs() < 1e-15,
2315            "wrapped sample must match in-branch sample"
2316        );
2317    }
2318
2319    fn grid_handle_parse_error(bytes: &[u8]) -> String {
2320        match GridHandle::from_bytes(test_grid_definition(), bytes) {
2321            Ok(_) => panic!("expected NTv2 parse failure"),
2322            Err(GridError::Parse(message)) => message,
2323            Err(error) => panic!("expected NTv2 parse error, got {error}"),
2324        }
2325    }
2326
2327    #[cfg(feature = "geotiff")]
2328    #[derive(Clone)]
2329    struct TestTiffTag {
2330        tag: u16,
2331        field_type: u16,
2332        count: u32,
2333        value: Vec<u8>,
2334    }
2335
2336    #[cfg(feature = "geotiff")]
2337    fn geotiff_parse_error(bytes: &[u8]) -> String {
2338        match parse_grid_data(GridFormat::GeoTiff, "test.tif", bytes) {
2339            Ok(_) => panic!("expected GeoTIFF parse failure"),
2340            Err(GridError::Parse(message)) => message,
2341            Err(error) => panic!("expected GeoTIFF parse error, got {error}"),
2342        }
2343    }
2344
2345    #[cfg(feature = "geotiff")]
2346    fn minimal_geotiff_bytes(width: u32, height: u32, bands: u16, grid_type: &str) -> Vec<u8> {
2347        classic_tiff(vec![minimal_geotiff_tags(width, height, bands, grid_type)])
2348    }
2349
2350    #[cfg(feature = "geotiff")]
2351    fn minimal_geotiff_tags(
2352        width: u32,
2353        height: u32,
2354        bands: u16,
2355        grid_type: &str,
2356    ) -> Vec<TestTiffTag> {
2357        vec![
2358            test_tiff_long(256, width),
2359            test_tiff_long(257, height),
2360            test_tiff_short(258, 32),
2361            test_tiff_short(259, 1),
2362            test_tiff_short(262, 1),
2363            test_tiff_short(277, bands),
2364            test_tiff_short(284, 1),
2365            test_tiff_short(339, 3),
2366            test_tiff_doubles(33550, &[1.0, 1.0, 0.0]),
2367            test_tiff_doubles(33922, &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
2368            test_tiff_shorts(34735, &[1, 1, 1, 0]),
2369            test_tiff_ascii(42112, grid_type),
2370        ]
2371    }
2372
2373    #[cfg(feature = "geotiff")]
2374    fn classic_tiff(mut ifds: Vec<Vec<TestTiffTag>>) -> Vec<u8> {
2375        for tags in &mut ifds {
2376            tags.sort_by_key(|tag| tag.tag);
2377        }
2378
2379        let block_lens: Vec<usize> = ifds
2380            .iter()
2381            .map(|tags| {
2382                let data_len = tags.iter().fold(0usize, |len, tag| {
2383                    if tag.value.len() <= 4 {
2384                        len
2385                    } else {
2386                        len + padded_tiff_value_len(tag.value.len())
2387                    }
2388                });
2389                2 + tags.len() * 12 + 4 + data_len
2390            })
2391            .collect();
2392        let mut starts = Vec::with_capacity(block_lens.len());
2393        let mut next_start = 8usize;
2394        for block_len in &block_lens {
2395            starts.push(next_start);
2396            next_start += block_len;
2397        }
2398
2399        let mut bytes = Vec::with_capacity(next_start);
2400        bytes.extend_from_slice(b"II");
2401        bytes.extend_from_slice(&42u16.to_le_bytes());
2402        bytes.extend_from_slice(&8u32.to_le_bytes());
2403
2404        for (ifd_index, tags) in ifds.iter().enumerate() {
2405            assert_eq!(bytes.len(), starts[ifd_index]);
2406            bytes.extend_from_slice(&(tags.len() as u16).to_le_bytes());
2407
2408            let data_start = starts[ifd_index] + 2 + tags.len() * 12 + 4;
2409            let mut data = Vec::new();
2410            for tag in tags {
2411                bytes.extend_from_slice(&tag.tag.to_le_bytes());
2412                bytes.extend_from_slice(&tag.field_type.to_le_bytes());
2413                bytes.extend_from_slice(&tag.count.to_le_bytes());
2414                if tag.value.len() <= 4 {
2415                    let mut inline = [0u8; 4];
2416                    inline[..tag.value.len()].copy_from_slice(&tag.value);
2417                    bytes.extend_from_slice(&inline);
2418                } else {
2419                    let offset = data_start + data.len();
2420                    bytes.extend_from_slice(&(offset as u32).to_le_bytes());
2421                    data.extend_from_slice(&tag.value);
2422                    if data.len() % 2 != 0 {
2423                        data.push(0);
2424                    }
2425                }
2426            }
2427
2428            let next_ifd = starts.get(ifd_index + 1).copied().unwrap_or(0);
2429            bytes.extend_from_slice(&(next_ifd as u32).to_le_bytes());
2430            bytes.extend_from_slice(&data);
2431        }
2432
2433        bytes
2434    }
2435
2436    #[cfg(feature = "geotiff")]
2437    fn padded_tiff_value_len(len: usize) -> usize {
2438        len + (len % 2)
2439    }
2440
2441    #[cfg(feature = "geotiff")]
2442    fn test_tiff_ascii(tag: u16, value: &str) -> TestTiffTag {
2443        let mut bytes = value.as_bytes().to_vec();
2444        if !bytes.ends_with(&[0]) {
2445            bytes.push(0);
2446        }
2447        TestTiffTag {
2448            tag,
2449            field_type: 2,
2450            count: bytes.len() as u32,
2451            value: bytes,
2452        }
2453    }
2454
2455    #[cfg(feature = "geotiff")]
2456    fn test_tiff_short(tag: u16, value: u16) -> TestTiffTag {
2457        test_tiff_shorts(tag, &[value])
2458    }
2459
2460    #[cfg(feature = "geotiff")]
2461    fn test_tiff_shorts(tag: u16, values: &[u16]) -> TestTiffTag {
2462        TestTiffTag {
2463            tag,
2464            field_type: 3,
2465            count: values.len() as u32,
2466            value: values
2467                .iter()
2468                .flat_map(|value| value.to_le_bytes())
2469                .collect(),
2470        }
2471    }
2472
2473    #[cfg(feature = "geotiff")]
2474    fn test_tiff_long(tag: u16, value: u32) -> TestTiffTag {
2475        TestTiffTag {
2476            tag,
2477            field_type: 4,
2478            count: 1,
2479            value: value.to_le_bytes().to_vec(),
2480        }
2481    }
2482
2483    #[cfg(feature = "geotiff")]
2484    fn test_tiff_doubles(tag: u16, values: &[f64]) -> TestTiffTag {
2485        TestTiffTag {
2486            tag,
2487            field_type: 12,
2488            count: values.len() as u32,
2489            value: values
2490                .iter()
2491                .flat_map(|value| value.to_le_bytes())
2492                .collect(),
2493        }
2494    }
2495
2496    fn test_temp_grid_root(name: &str) -> PathBuf {
2497        static NEXT_ROOT: AtomicUsize = AtomicUsize::new(0);
2498
2499        let root = std::env::temp_dir().join(format!(
2500            "proj-core-{name}-{}-{}",
2501            std::process::id(),
2502            NEXT_ROOT.fetch_add(1, Ordering::SeqCst)
2503        ));
2504        let _ = std::fs::remove_dir_all(&root);
2505        std::fs::create_dir_all(&root).unwrap();
2506        root
2507    }
2508
2509    #[test]
2510    fn ntv2_rejects_oversized_resource_length_before_reading() {
2511        let message = match validate_grid_resource_size(
2512            "oversized.gsb",
2513            GridFormat::Ntv2,
2514            MAX_NTV2_GRID_BYTES as u64 + 1,
2515        ) {
2516            Ok(()) => panic!("expected NTv2 resource size failure"),
2517            Err(GridError::Parse(message)) => message,
2518            Err(error) => panic!("expected NTv2 parse error, got {error}"),
2519        };
2520
2521        assert!(message.contains("maximum Ntv2 grid size"), "{message}");
2522    }
2523
2524    #[test]
2525    fn unsupported_grid_format_is_rejected_before_resource_reading() {
2526        let root = test_temp_grid_root("unsupported-format-read");
2527        let path = root.join("unsupported.grid");
2528        std::fs::write(&path, b"untrusted bytes").unwrap();
2529        let file = std::fs::File::open(&path).unwrap();
2530
2531        let error = read_grid_resource_file(file, &path, GridFormat::Unsupported).unwrap_err();
2532
2533        assert!(matches!(error, GridError::UnsupportedFormat(_)));
2534    }
2535
2536    #[test]
2537    fn grid_handle_rejects_excessive_ntv2_subfile_count_before_allocation() {
2538        let mut bytes = vec![0u8; NTV2_HEADER_LEN];
2539        write_ntv2_global_header(&mut bytes, u32::MAX);
2540
2541        let message = grid_handle_parse_error(&bytes);
2542
2543        assert!(message.contains("subfile count"), "{message}");
2544    }
2545
2546    #[test]
2547    fn ntv2_rejects_excessive_axis_count_before_cell_multiplication() {
2548        let mut bytes = minimal_ntv2_bytes();
2549        let header = &mut bytes[NTV2_HEADER_LEN..NTV2_HEADER_LEN * 2];
2550        write_ntv2_f64(header, 120, MAX_NTV2_CELLS_PER_SUBGRID as f64);
2551        write_ntv2_f64(header, 152, 1.0);
2552
2553        let message = grid_handle_parse_error(&bytes);
2554
2555        assert!(
2556            message.contains("longitude cell count exceeds limit"),
2557            "{message}"
2558        );
2559    }
2560
2561    #[test]
2562    fn ntv2_rejects_excessive_subgrid_cell_count_before_allocation() {
2563        let mut bytes = minimal_ntv2_bytes();
2564        let header = &mut bytes[NTV2_HEADER_LEN..NTV2_HEADER_LEN * 2];
2565        write_ntv2_f64(header, 88, 4096.0);
2566        write_ntv2_f64(header, 120, 4096.0);
2567        write_ntv2_f64(header, 136, 1.0);
2568        write_ntv2_f64(header, 152, 1.0);
2569        write_ntv2_u32(header, 168, 16_785_409);
2570
2571        let message = grid_handle_parse_error(&bytes);
2572
2573        assert!(message.contains("exceeding limit"), "{message}");
2574    }
2575
2576    #[test]
2577    fn ntv2_rejects_non_finite_shift_values() {
2578        let mut bytes = minimal_ntv2_bytes();
2579        write_ntv2_f32(&mut bytes, NTV2_HEADER_LEN * 2, f32::NAN);
2580
2581        let message = grid_handle_parse_error(&bytes);
2582
2583        assert!(message.contains("non-finite NTv2 shift value"), "{message}");
2584    }
2585
2586    #[cfg(feature = "geotiff")]
2587    #[test]
2588    fn geotiff_rejects_excessive_ifd_count_before_decoding() {
2589        let bytes = classic_tiff(vec![Vec::new(); MAX_GEOTIFF_IFDS + 1]);
2590
2591        let message = geotiff_parse_error(&bytes);
2592
2593        assert!(message.contains("IFD"), "{message}");
2594        assert!(message.contains(&MAX_GEOTIFF_IFDS.to_string()), "{message}");
2595    }
2596
2597    #[cfg(feature = "geotiff")]
2598    #[test]
2599    fn geotiff_rejects_excessive_axis_dimensions_before_decoding() {
2600        for (width, height, expected) in [
2601            ((MAX_GEOTIFF_CELLS_PER_IMAGE + 1) as u32, 2, "width"),
2602            (2, (MAX_GEOTIFF_CELLS_PER_IMAGE + 1) as u32, "height"),
2603        ] {
2604            let bytes = minimal_geotiff_bytes(width, height, 1, "TYPE=VERTICAL_OFFSET");
2605
2606            let message = geotiff_parse_error(&bytes);
2607
2608            assert!(message.contains(expected), "{message}");
2609            assert!(message.contains("exceeds limit"), "{message}");
2610        }
2611    }
2612
2613    #[cfg(feature = "geotiff")]
2614    #[test]
2615    fn geotiff_rejects_excessive_cell_count_before_decoding() {
2616        let bytes = minimal_geotiff_bytes(4097, 4097, 1, "TYPE=VERTICAL_OFFSET");
2617
2618        let message = geotiff_parse_error(&bytes);
2619
2620        assert!(message.contains("cell count"), "{message}");
2621        assert!(message.contains("exceeds limit"), "{message}");
2622    }
2623
2624    #[cfg(feature = "geotiff")]
2625    #[test]
2626    fn geotiff_rejects_excessive_band_count_before_decoding() {
2627        let bytes =
2628            minimal_geotiff_bytes(2, 2, (MAX_GEOTIFF_BANDS + 1) as u16, "TYPE=VERTICAL_OFFSET");
2629
2630        let message = geotiff_parse_error(&bytes);
2631
2632        assert!(message.contains("band count"), "{message}");
2633        assert!(message.contains("exceeds limit"), "{message}");
2634    }
2635
2636    #[cfg(feature = "geotiff")]
2637    #[test]
2638    fn geotiff_rejects_horizontal_grid_with_too_few_bands_before_decoding() {
2639        let bytes = minimal_geotiff_bytes(2, 2, 1, "TYPE=HORIZONTAL_OFFSET");
2640
2641        let message = geotiff_parse_error(&bytes);
2642
2643        assert!(message.contains("needs at least 2"), "{message}");
2644    }
2645
2646    #[cfg(feature = "geotiff")]
2647    #[test]
2648    fn geotiff_rejects_excessive_total_cell_count_before_decoding() {
2649        let image = minimal_geotiff_tags(4096, 4096, 2, "TYPE=HORIZONTAL_OFFSET");
2650        let bytes = classic_tiff(vec![image.clone(), image]);
2651
2652        let message = geotiff_parse_error(&bytes);
2653
2654        assert!(message.contains("total cell count"), "{message}");
2655        assert!(message.contains("exceeds limit"), "{message}");
2656    }
2657
2658    proptest! {
2659        #![proptest_config(ProptestConfig::with_cases(256))]
2660
2661        #[test]
2662        fn ntv2_malformed_subfile_header_fuzz_does_not_panic(
2663            name in proptest::collection::vec(any::<u8>(), 8),
2664            parent in proptest::collection::vec(any::<u8>(), 8),
2665            south_bits in any::<u64>(),
2666            north_bits in any::<u64>(),
2667            east_bits in any::<u64>(),
2668            west_bits in any::<u64>(),
2669            res_y_bits in any::<u64>(),
2670            res_x_bits in any::<u64>(),
2671            gs_count in any::<u32>(),
2672            data in proptest::collection::vec(any::<u8>(), 0..512),
2673        ) {
2674            let mut bytes = vec![0u8; NTV2_HEADER_LEN * 2];
2675            write_ntv2_global_header(&mut bytes[..NTV2_HEADER_LEN], 1);
2676
2677            let header = &mut bytes[NTV2_HEADER_LEN..NTV2_HEADER_LEN * 2];
2678            header[0..8].copy_from_slice(b"SUB_NAME");
2679            header[8..16].copy_from_slice(&name);
2680            header[24..32].copy_from_slice(&parent);
2681            write_ntv2_f64_bits(header, 72, south_bits);
2682            write_ntv2_f64_bits(header, 88, north_bits);
2683            write_ntv2_f64_bits(header, 104, east_bits);
2684            write_ntv2_f64_bits(header, 120, west_bits);
2685            write_ntv2_f64_bits(header, 136, res_y_bits);
2686            write_ntv2_f64_bits(header, 152, res_x_bits);
2687            write_ntv2_u32(header, 168, gs_count);
2688            bytes.extend_from_slice(&data);
2689
2690            let _ = Ntv2GridSet::parse(&bytes);
2691        }
2692    }
2693
2694    #[test]
2695    fn filesystem_provider_rejects_unsafe_resource_names() {
2696        let root = test_temp_grid_root("unsafe-resource");
2697        std::fs::write(root.join("safe.gtx"), []).unwrap();
2698
2699        let provider = FilesystemGridProvider::new(vec![root.clone()]);
2700        let mut definition = test_grid_definition();
2701        definition.format = GridFormat::Gtx;
2702        definition.resource_names = SmallVec::from_vec(vec!["../safe.gtx".into()]);
2703        assert!(provider.definition(&definition).unwrap().is_none());
2704
2705        definition.resource_names =
2706            SmallVec::from_vec(vec![root.join("safe.gtx").to_string_lossy().into_owned()]);
2707        assert!(provider.definition(&definition).unwrap().is_none());
2708
2709        definition.resource_names = SmallVec::from_vec(vec!["safe.gtx".into()]);
2710        assert!(provider.definition(&definition).unwrap().is_some());
2711    }
2712
2713    #[test]
2714    fn filesystem_provider_loads_grid_from_canonical_root() {
2715        let root = test_temp_grid_root("canonical-root");
2716        std::fs::write(
2717            root.join("safe.gtx"),
2718            test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]),
2719        )
2720        .unwrap();
2721
2722        let provider = FilesystemGridProvider::new(vec![root]);
2723        let mut definition = test_grid_definition();
2724        definition.name = "safe.gtx".into();
2725        definition.format = GridFormat::Gtx;
2726        definition.resource_names = SmallVec::from_vec(vec!["safe.gtx".into()]);
2727
2728        assert!(provider.definition(&definition).unwrap().is_some());
2729        let handle = provider.load(&definition).unwrap().unwrap();
2730        let sample = handle
2731            .sample_vertical_offset_meters(20.5f64.to_radians(), 10.5f64.to_radians())
2732            .unwrap();
2733
2734        assert!((sample.offset_meters - 2.0).abs() < 1e-12);
2735    }
2736
2737    #[test]
2738    fn filesystem_provider_reuses_located_path_between_definition_and_load() {
2739        let root = test_temp_grid_root("path-cache");
2740        std::fs::write(
2741            root.join("cached.gtx"),
2742            test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]),
2743        )
2744        .unwrap();
2745
2746        let provider = FilesystemGridProvider::new(vec![root]);
2747        let mut definition = test_grid_definition();
2748        definition.name = "cached.gtx".into();
2749        definition.format = GridFormat::Gtx;
2750        definition.resource_names = SmallVec::from_vec(vec!["cached.gtx".into()]);
2751
2752        assert!(provider.definition(&definition).unwrap().is_some());
2753        assert_eq!(provider.locate_searches.load(Ordering::SeqCst), 1);
2754
2755        assert!(provider.load(&definition).unwrap().is_some());
2756        assert_eq!(provider.locate_searches.load(Ordering::SeqCst), 1);
2757    }
2758
2759    #[test]
2760    fn filesystem_provider_cache_key_distinguishes_resource_boundaries() {
2761        let root = test_temp_grid_root("resource-boundary-cache-key");
2762        std::fs::write(root.join("a|b"), []).unwrap();
2763        std::fs::write(root.join("a"), []).unwrap();
2764
2765        let provider = FilesystemGridProvider::new(vec![root]);
2766        let mut delimited = test_grid_definition();
2767        delimited.resource_names = SmallVec::from_vec(vec!["a|b".into()]);
2768        let mut split = test_grid_definition();
2769        split.resource_names = SmallVec::from_vec(vec!["a".into(), "b".into()]);
2770
2771        let delimited_location = provider.locate(&delimited).unwrap();
2772        let split_location = provider.locate(&split).unwrap();
2773
2774        assert_eq!(
2775            delimited_location.path.file_name().unwrap(),
2776            std::ffi::OsStr::new("a|b")
2777        );
2778        assert_eq!(
2779            split_location.path.file_name().unwrap(),
2780            std::ffi::OsStr::new("a")
2781        );
2782        assert_eq!(provider.locate_searches.load(Ordering::SeqCst), 2);
2783    }
2784
2785    #[test]
2786    fn grid_runtime_cache_key_covers_definition_metadata() {
2787        let base = test_grid_definition();
2788        let mut renamed = base.clone();
2789        renamed.name = "different diagnostic name".into();
2790        let mut rescoped = base.clone();
2791        rescoped.area_of_use = Some(AreaOfUse {
2792            west: -10.0,
2793            south: -5.0,
2794            east: 10.0,
2795            north: 5.0,
2796            name: "test extent".into(),
2797        });
2798
2799        assert_ne!(
2800            grid_runtime_cache_key(&base),
2801            grid_runtime_cache_key(&renamed)
2802        );
2803        assert_ne!(
2804            grid_runtime_cache_key(&base),
2805            grid_runtime_cache_key(&rescoped)
2806        );
2807    }
2808
2809    #[cfg(unix)]
2810    #[test]
2811    fn filesystem_provider_rejects_cached_path_swapped_to_symlink() {
2812        use std::os::unix::fs::symlink;
2813
2814        let root = test_temp_grid_root("stale-path-cache");
2815        let outside = test_temp_grid_root("stale-path-outside");
2816        let grid_path = root.join("cached.gtx");
2817        let outside_path = outside.join("outside.gtx");
2818        std::fs::write(
2819            &grid_path,
2820            test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]),
2821        )
2822        .unwrap();
2823        std::fs::write(
2824            &outside_path,
2825            test_gtx_bytes(&[
2826                100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0,
2827            ]),
2828        )
2829        .unwrap();
2830
2831        let provider = FilesystemGridProvider::new(vec![root]);
2832        let mut definition = test_grid_definition();
2833        definition.name = "cached.gtx".into();
2834        definition.format = GridFormat::Gtx;
2835        definition.resource_names = SmallVec::from_vec(vec!["cached.gtx".into()]);
2836
2837        assert!(provider.definition(&definition).unwrap().is_some());
2838        assert_eq!(provider.locate_searches.load(Ordering::SeqCst), 1);
2839
2840        std::fs::remove_file(&grid_path).unwrap();
2841        symlink(&outside_path, &grid_path).unwrap();
2842
2843        assert!(provider.load(&definition).unwrap().is_none());
2844        assert_eq!(provider.locate_searches.load(Ordering::SeqCst), 2);
2845    }
2846
2847    #[cfg(unix)]
2848    #[test]
2849    fn filesystem_grid_open_rejects_symlink_after_canonicalization() {
2850        use std::os::unix::fs::symlink;
2851
2852        let root = test_temp_grid_root("nofollow-open");
2853        let outside = test_temp_grid_root("nofollow-open-outside");
2854        let grid_path = root.join("cached.gtx");
2855        let outside_path = outside.join("outside.gtx");
2856        std::fs::write(&grid_path, test_gtx_bytes(&[0.0; 9])).unwrap();
2857        std::fs::write(&outside_path, test_gtx_bytes(&[100.0; 9])).unwrap();
2858
2859        let location = FilesystemGridLocation {
2860            root: root.canonicalize().unwrap(),
2861            path: grid_path.canonicalize().unwrap(),
2862        };
2863        let canonical_path = location.path.clone();
2864
2865        std::fs::remove_file(&grid_path).unwrap();
2866        symlink(&outside_path, &grid_path).unwrap();
2867
2868        let err = open_filesystem_grid_resource_file(&location, &canonical_path).unwrap_err();
2869        assert!(matches!(err, GridError::Unavailable(_)));
2870    }
2871
2872    #[test]
2873    fn filesystem_grid_read_enforces_cap_on_bytes_read() {
2874        let root = test_temp_grid_root("bounded-read");
2875        let path = root.join("oversized.gtx");
2876        std::fs::write(&path, [0u8; 4]).unwrap();
2877
2878        let err = read_bounded_grid_resource_bytes(&path, GridFormat::Gtx, 3).unwrap_err();
2879
2880        let GridError::Parse(message) = err else {
2881            panic!("expected parse error");
2882        };
2883        assert!(
2884            message.contains("maximum Gtx grid size of 3 bytes"),
2885            "{message}"
2886        );
2887    }
2888
2889    fn test_gtx_bytes(values: &[f32]) -> Vec<u8> {
2890        let mut bytes = Vec::new();
2891        write_gtx_header(&mut bytes, 3, 3);
2892        for value in values {
2893            bytes.extend_from_slice(&value.to_be_bytes());
2894        }
2895        bytes
2896    }
2897
2898    fn write_gtx_header(bytes: &mut Vec<u8>, height: i32, width: i32) {
2899        bytes.extend_from_slice(&10.0f64.to_be_bytes());
2900        bytes.extend_from_slice(&20.0f64.to_be_bytes());
2901        bytes.extend_from_slice(&1.0f64.to_be_bytes());
2902        bytes.extend_from_slice(&1.0f64.to_be_bytes());
2903        bytes.extend_from_slice(&height.to_be_bytes());
2904        bytes.extend_from_slice(&width.to_be_bytes());
2905    }
2906
2907    fn gtx_parse_error(bytes: &[u8]) -> String {
2908        match parse_grid_data(GridFormat::Gtx, "test.gtx", bytes) {
2909            Ok(_) => panic!("expected GTX parse failure"),
2910            Err(GridError::Parse(message)) => message,
2911            Err(error) => panic!("expected GTX parse error, got {error}"),
2912        }
2913    }
2914
2915    #[test]
2916    fn gtx_rejects_excessive_dimensions_before_allocation() {
2917        let mut bytes = Vec::new();
2918        write_gtx_header(&mut bytes, 4097, 4097);
2919
2920        let message = gtx_parse_error(&bytes);
2921
2922        assert!(message.contains("GTX cell count"), "{message}");
2923        assert!(message.contains("exceeds limit"), "{message}");
2924    }
2925
2926    #[test]
2927    fn gtx_rejects_oversized_resource_length_before_reading() {
2928        let message = match validate_grid_resource_size(
2929            "oversized.gtx",
2930            GridFormat::Gtx,
2931            MAX_GTX_GRID_BYTES as u64 + 1,
2932        ) {
2933            Ok(()) => panic!("expected GTX resource size failure"),
2934            Err(GridError::Parse(message)) => message,
2935            Err(error) => panic!("expected GTX parse error, got {error}"),
2936        };
2937
2938        assert!(message.contains("maximum Gtx grid size"), "{message}");
2939    }
2940
2941    #[test]
2942    fn gtx_truncated_data_remains_parse_error() {
2943        let mut bytes = Vec::new();
2944        write_gtx_header(&mut bytes, 3, 3);
2945
2946        let message = gtx_parse_error(&bytes);
2947
2948        assert!(message.contains("truncated GTX data"), "{message}");
2949    }
2950
2951    #[test]
2952    fn gtx_grid_samples_bilinear_offsets() {
2953        let bytes = test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
2954        let data = parse_grid_data(GridFormat::Gtx, "test.gtx", &bytes).unwrap();
2955        let GridData::Gtx(grid) = data else {
2956            panic!("expected GTX grid");
2957        };
2958
2959        let sample = grid
2960            .sample(20.5f64.to_radians(), 10.5f64.to_radians())
2961            .unwrap();
2962        assert!((sample.offset_meters - 2.0).abs() < 1e-12);
2963
2964        let wrapped_sample = grid
2965            .sample(
2966                (20.5 + 360.0 * 1_000_000_000_000.0f64).to_radians(),
2967                10.5f64.to_radians(),
2968            )
2969            .unwrap();
2970        assert!((wrapped_sample.offset_meters - 2.0).abs() < 1e-12);
2971
2972        let lower_edge_sample = grid
2973            .sample(
2974                (20.0 - 5e-11f64).to_radians(),
2975                (10.0 - 5e-11f64).to_radians(),
2976            )
2977            .unwrap();
2978        assert!((lower_edge_sample.offset_meters - 0.0).abs() < 1e-12);
2979    }
2980
2981    #[test]
2982    fn gtx_grid_rejects_outside_or_null_cells() {
2983        let bytes = test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, -88.8888, 5.0, 6.0, 7.0, 8.0]);
2984        let data = parse_grid_data(GridFormat::Gtx, "test.gtx", &bytes).unwrap();
2985        let GridData::Gtx(grid) = data else {
2986            panic!("expected GTX grid");
2987        };
2988
2989        let null_err = grid
2990            .sample(20.5f64.to_radians(), 10.5f64.to_radians())
2991            .unwrap_err();
2992        assert!(matches!(null_err, GridError::OutsideCoverage(_)));
2993
2994        let outside_err = grid
2995            .sample(30.0f64.to_radians(), 10.5f64.to_radians())
2996            .unwrap_err();
2997        assert!(matches!(outside_err, GridError::OutsideCoverage(_)));
2998    }
2999
3000    #[test]
3001    fn gtx_grid_rejects_non_finite_coordinates() {
3002        let bytes = test_gtx_bytes(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
3003        let data = parse_grid_data(GridFormat::Gtx, "test.gtx", &bytes).unwrap();
3004        let GridData::Gtx(grid) = data else {
3005            panic!("expected GTX grid");
3006        };
3007
3008        for (lon, lat) in [
3009            (f64::INFINITY, 10.5f64.to_radians()),
3010            (f64::NEG_INFINITY, 10.5f64.to_radians()),
3011            (f64::NAN, 10.5f64.to_radians()),
3012            (20.5f64.to_radians(), f64::INFINITY),
3013            (20.5f64.to_radians(), f64::NAN),
3014        ] {
3015            let err = grid.sample(lon, lat).unwrap_err();
3016            assert!(matches!(err, GridError::OutsideCoverage(_)));
3017            let message = err.to_string();
3018            assert!(message.contains("non-finite"), "{message}");
3019        }
3020    }
3021
3022    #[test]
3023    fn app_grid_provider_can_override_embedded_grid() {
3024        let definition_calls = Arc::new(AtomicUsize::new(0));
3025        let load_calls = Arc::new(AtomicUsize::new(0));
3026        let provider = TrackingGridProvider {
3027            override_definition: true,
3028            definition_calls: Arc::clone(&definition_calls),
3029            load_calls: Arc::clone(&load_calls),
3030        };
3031        let runtime = GridRuntime::new(Some(Arc::new(provider)));
3032
3033        let handle = runtime
3034            .resolve_handle(&test_grid_definition())
3035            .expect("grid should resolve");
3036
3037        assert_eq!(handle.definition().name, "custom override");
3038        assert_eq!(definition_calls.load(Ordering::SeqCst), 1);
3039        assert_eq!(load_calls.load(Ordering::SeqCst), 1);
3040    }
3041
3042    #[test]
3043    fn app_grid_provider_falls_back_to_embedded_grid() {
3044        let definition_calls = Arc::new(AtomicUsize::new(0));
3045        let load_calls = Arc::new(AtomicUsize::new(0));
3046        let provider = TrackingGridProvider {
3047            override_definition: false,
3048            definition_calls: Arc::clone(&definition_calls),
3049            load_calls: Arc::clone(&load_calls),
3050        };
3051        let runtime = GridRuntime::new(Some(Arc::new(provider)));
3052
3053        let handle = runtime
3054            .resolve_handle(&test_grid_definition())
3055            .expect("embedded grid should remain available");
3056
3057        assert_eq!(handle.definition().name, "ntv2_0.gsb");
3058        assert_eq!(definition_calls.load(Ordering::SeqCst), 1);
3059        assert_eq!(load_calls.load(Ordering::SeqCst), 1);
3060    }
3061}