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