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