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