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