use std::path::{Path, PathBuf};
use oxigdal_core::error::{IoError, OxiGdalError};
use crate::magic::{BIGTIFF_VERSION, TIFF_VERSION};
use crate::{DatasetFormat, DatasetInfo, Result};
fn detect_cloud_scheme(path_str: &str) -> Option<CloudScheme> {
if path_str.starts_with("s3://") {
Some(CloudScheme::S3)
} else if path_str.starts_with("gs://") {
Some(CloudScheme::Gcs)
} else if path_str.starts_with("az://") || path_str.starts_with("abfs://") {
Some(CloudScheme::Azure)
} else if path_str.starts_with("http://") || path_str.starts_with("https://") {
Some(CloudScheme::Http)
} else {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CloudScheme {
S3,
Gcs,
Azure,
Http,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum MagicDetectionResult {
Detected(DatasetFormat),
Unknown,
}
fn read_magic_bytes(path: &Path, n: usize) -> Result<Vec<u8>> {
use std::io::Read;
let mut file = std::fs::File::open(path).map_err(|e| {
OxiGdalError::Io(IoError::Read {
message: format!("cannot open '{}': {e}", path.display()),
})
})?;
let mut buf = vec![0u8; n];
let read_bytes = file.read(&mut buf).map_err(|e| {
OxiGdalError::Io(IoError::Read {
message: format!("cannot read magic bytes from '{}': {e}", path.display()),
})
})?;
buf.truncate(read_bytes);
Ok(buf)
}
fn detect_from_magic(path: &Path) -> Result<MagicDetectionResult> {
use crate::magic::MAGIC_READ_SIZE;
let buf = read_magic_bytes(path, MAGIC_READ_SIZE)?;
match DatasetFormat::detect_from_magic_bytes(&buf) {
Some(fmt) => Ok(MagicDetectionResult::Detected(fmt)),
None => Ok(MagicDetectionResult::Unknown),
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum OpenedDataset {
GeoTiff(DatasetInfo),
GeoJson(DatasetInfo),
Shapefile(DatasetInfo),
GeoPackage(DatasetInfo),
GeoParquet(DatasetInfo),
NetCdf(DatasetInfo),
Hdf5(DatasetInfo),
Zarr(DatasetInfo),
Grib(DatasetInfo),
FlatGeobuf(DatasetInfo),
Jpeg2000(DatasetInfo),
Vrt(DatasetInfo),
Stac(DatasetInfo),
Cloud {
scheme: CloudScheme,
path: PathBuf,
guessed_format: DatasetFormat,
},
Unknown(DatasetInfo),
}
impl OpenedDataset {
pub fn info(&self) -> Option<&DatasetInfo> {
match self {
Self::GeoTiff(i)
| Self::GeoJson(i)
| Self::Shapefile(i)
| Self::GeoPackage(i)
| Self::GeoParquet(i)
| Self::NetCdf(i)
| Self::Hdf5(i)
| Self::Zarr(i)
| Self::Grib(i)
| Self::FlatGeobuf(i)
| Self::Jpeg2000(i)
| Self::Vrt(i)
| Self::Stac(i)
| Self::Unknown(i) => Some(i),
Self::Cloud { .. } => None,
}
}
pub fn format(&self) -> DatasetFormat {
match self {
Self::GeoTiff(_) => DatasetFormat::GeoTiff,
Self::GeoJson(_) => DatasetFormat::GeoJson,
Self::Shapefile(_) => DatasetFormat::Shapefile,
Self::GeoPackage(_) => DatasetFormat::GeoPackage,
Self::GeoParquet(_) => DatasetFormat::GeoParquet,
Self::NetCdf(_) => DatasetFormat::NetCdf,
Self::Hdf5(_) => DatasetFormat::Hdf5,
Self::Zarr(_) => DatasetFormat::Zarr,
Self::Grib(_) => DatasetFormat::Grib,
Self::FlatGeobuf(_) => DatasetFormat::FlatGeobuf,
Self::Jpeg2000(_) => DatasetFormat::Jpeg2000,
Self::Vrt(_) => DatasetFormat::Vrt,
Self::Stac(_) => DatasetFormat::Stac,
Self::Cloud { guessed_format, .. } => *guessed_format,
Self::Unknown(_) => DatasetFormat::Unknown,
}
}
pub fn is_cloud(&self) -> bool {
matches!(self, Self::Cloud { .. })
}
pub fn is_raster(&self) -> bool {
matches!(
self,
Self::GeoTiff(_)
| Self::Jpeg2000(_)
| Self::NetCdf(_)
| Self::Hdf5(_)
| Self::Zarr(_)
| Self::Grib(_)
| Self::Vrt(_)
)
}
pub fn is_vector(&self) -> bool {
matches!(
self,
Self::GeoJson(_)
| Self::Shapefile(_)
| Self::GeoPackage(_)
| Self::GeoParquet(_)
| Self::FlatGeobuf(_)
| Self::Stac(_)
)
}
}
pub fn open(path: impl AsRef<Path>) -> Result<OpenedDataset> {
let path_ref = path.as_ref();
let path_str = path_ref.to_str().unwrap_or("").to_string();
if let Some(scheme) = detect_cloud_scheme(&path_str) {
let guessed_format = DatasetFormat::from_extension(&path_str);
return Ok(OpenedDataset::Cloud {
scheme,
path: path_ref.to_path_buf(),
guessed_format,
});
}
if !path_ref.exists() {
return Err(OxiGdalError::Io(IoError::NotFound {
path: path_str.clone(),
}));
}
let magic_result = detect_from_magic(path_ref)?;
let format = match magic_result {
MagicDetectionResult::Detected(fmt) => {
if fmt == DatasetFormat::GeoPackage {
let ext_fmt = DatasetFormat::from_extension(&path_str);
match ext_fmt {
DatasetFormat::Unknown => DatasetFormat::GeoPackage,
other => other,
}
} else {
fmt
}
}
MagicDetectionResult::Unknown => {
let ext_fmt = DatasetFormat::from_extension(&path_str);
if ext_fmt == DatasetFormat::Unknown {
let ext = path_ref
.extension()
.and_then(|e| e.to_str())
.map(str::to_lowercase)
.unwrap_or_default();
if ext == "json" {
DatasetFormat::GeoJson
} else {
DatasetFormat::Unknown
}
} else {
ext_fmt
}
}
};
let info = build_dataset_info(path_ref, format);
let opened = map_format_to_opened(format, info);
Ok(opened)
}
fn build_dataset_info(path: &Path, format: DatasetFormat) -> DatasetInfo {
let path_str = path.to_str().map(str::to_string);
let empty = |fmt: DatasetFormat| DatasetInfo {
format: fmt,
path: path_str.clone(),
width: None,
height: None,
band_count: 0,
layer_count: 0,
crs: None,
geotransform: None,
feature_count: None,
bounds: None,
};
match format {
DatasetFormat::GeoTiff => {
let mut info = extract_tiff_info(path).unwrap_or_else(|| empty(format));
info.path = path_str;
info
}
DatasetFormat::GeoJson => {
let mut info = extract_geojson_info(path).unwrap_or_else(|| empty(format));
info.path = path_str;
info
}
#[cfg(feature = "shapefile")]
DatasetFormat::Shapefile => {
let mut info = extract_shapefile_info(path).unwrap_or_else(|| empty(format));
info.path = path_str;
info
}
#[cfg(feature = "flatgeobuf")]
DatasetFormat::FlatGeobuf => {
let mut info = extract_flatgeobuf_info(path).unwrap_or_else(|| empty(format));
info.path = path_str;
info
}
#[cfg(feature = "geoparquet")]
DatasetFormat::GeoParquet => {
let mut info = extract_geoparquet_info(path).unwrap_or_else(|| empty(format));
info.path = path_str;
info
}
_ => empty(format),
}
}
const TAG_IMAGE_WIDTH: u16 = 256;
const TAG_IMAGE_LENGTH: u16 = 257;
const TAG_SAMPLES_PER_PIXEL: u16 = 277;
const TAG_MODEL_PIXEL_SCALE: u16 = 33550;
const TAG_MODEL_TIEPOINT: u16 = 33922;
const TAG_GEO_KEY_DIRECTORY: u16 = 34735;
fn tiff_read_u16(buf: &[u8], offset: usize, le: bool) -> Option<u16> {
if offset + 2 > buf.len() {
return None;
}
Some(if le {
u16::from_le_bytes([buf[offset], buf[offset + 1]])
} else {
u16::from_be_bytes([buf[offset], buf[offset + 1]])
})
}
fn tiff_read_u32(buf: &[u8], offset: usize, le: bool) -> Option<u32> {
if offset + 4 > buf.len() {
return None;
}
Some(if le {
u32::from_le_bytes([
buf[offset],
buf[offset + 1],
buf[offset + 2],
buf[offset + 3],
])
} else {
u32::from_be_bytes([
buf[offset],
buf[offset + 1],
buf[offset + 2],
buf[offset + 3],
])
})
}
fn tiff_read_u64(buf: &[u8], offset: usize, le: bool) -> Option<u64> {
if offset + 8 > buf.len() {
return None;
}
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&buf[offset..offset + 8]);
Some(if le {
u64::from_le_bytes(bytes)
} else {
u64::from_be_bytes(bytes)
})
}
fn tiff_read_f64(buf: &[u8], offset: usize, le: bool) -> Option<f64> {
if offset + 8 > buf.len() {
return None;
}
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&buf[offset..offset + 8]);
Some(if le {
f64::from_le_bytes(bytes)
} else {
f64::from_be_bytes(bytes)
})
}
fn ifd_entry_value_u32(buf: &[u8], entry_offset: usize, le: bool) -> Option<u32> {
let type_id = tiff_read_u16(buf, entry_offset + 2, le)?;
match type_id {
3 => tiff_read_u16(buf, entry_offset + 8, le).map(u32::from),
4 => tiff_read_u32(buf, entry_offset + 8, le),
_ => None,
}
}
pub(crate) fn extract_tiff_info(path: &Path) -> Option<DatasetInfo> {
use std::io::Read;
let mut file = std::fs::File::open(path).ok()?;
let mut buf = vec![0u8; 8192];
let n = file.read(&mut buf).ok()?;
buf.truncate(n);
if buf.len() < 8 {
return None;
}
let le = buf[0] == 0x49;
let version = tiff_read_u16(&buf, 2, le)?;
let (ifd_offset, entry_size) = if version == BIGTIFF_VERSION {
let off = tiff_read_u64(&buf, 8, le)? as usize;
(off, 20usize) } else if version == TIFF_VERSION {
let off = tiff_read_u32(&buf, 4, le)? as usize;
(off, 12usize) } else {
return None; };
let num_entries = if version == BIGTIFF_VERSION {
tiff_read_u64(&buf, ifd_offset, le)? as usize
} else {
tiff_read_u16(&buf, ifd_offset, le)? as usize
};
let entries_start = if version == BIGTIFF_VERSION {
ifd_offset + 8
} else {
ifd_offset + 2
};
let mut width: Option<u32> = None;
let mut height: Option<u32> = None;
let mut samples_per_pixel: u32 = 1;
let mut pixel_scale_offset: Option<usize> = None;
let mut tiepoint_offset: Option<usize> = None;
let mut _geo_keys_found = false;
for i in 0..num_entries {
let eo = entries_start + i * entry_size;
if eo + entry_size > buf.len() {
break;
}
let tag = tiff_read_u16(&buf, eo, le)?;
match tag {
TAG_IMAGE_WIDTH => {
width = ifd_entry_value_u32(&buf, eo, le);
}
TAG_IMAGE_LENGTH => {
height = ifd_entry_value_u32(&buf, eo, le);
}
TAG_SAMPLES_PER_PIXEL => {
if let Some(v) = ifd_entry_value_u32(&buf, eo, le) {
samples_per_pixel = v;
}
}
TAG_MODEL_PIXEL_SCALE => {
let off = tiff_read_u32(&buf, eo + 8, le)? as usize;
pixel_scale_offset = Some(off);
}
TAG_MODEL_TIEPOINT => {
let off = tiff_read_u32(&buf, eo + 8, le)? as usize;
tiepoint_offset = Some(off);
}
TAG_GEO_KEY_DIRECTORY => {
_geo_keys_found = true;
}
_ => {}
}
}
let geotransform = match (pixel_scale_offset, tiepoint_offset) {
(Some(ps_off), Some(tp_off)) if ps_off + 24 <= buf.len() && tp_off + 48 <= buf.len() => {
let scale_x = tiff_read_f64(&buf, ps_off, le)?;
let scale_y = tiff_read_f64(&buf, ps_off + 8, le)?;
let _i = tiff_read_f64(&buf, tp_off, le)?;
let _j = tiff_read_f64(&buf, tp_off + 8, le)?;
let origin_x = tiff_read_f64(&buf, tp_off + 24, le)?;
let origin_y = tiff_read_f64(&buf, tp_off + 32, le)?;
if scale_x.is_finite() && scale_y.is_finite() && scale_x > 0.0 && scale_y > 0.0 {
Some(oxigdal_core::types::GeoTransform::north_up(
origin_x, origin_y, scale_x, scale_y,
))
} else {
None
}
}
_ => None,
};
Some(DatasetInfo {
format: DatasetFormat::GeoTiff,
path: None, width,
height,
band_count: samples_per_pixel,
layer_count: 0,
crs: None,
geotransform,
feature_count: None,
bounds: None,
})
}
pub(crate) fn extract_geojson_info(path: &Path) -> Option<DatasetInfo> {
use std::io::Read;
let mut file = std::fs::File::open(path).ok()?;
let mut buf = vec![0u8; 65536];
let n = file.read(&mut buf).ok()?;
buf.truncate(n);
let text = std::str::from_utf8(&buf).ok()?;
let is_collection = text.contains("\"FeatureCollection\"");
let layer_count = if is_collection { 1 } else { 0 };
let feature_count = if is_collection {
let count = count_geojson_features(text);
if count > 0 { Some(count as u64) } else { None }
} else {
None
};
let bounds = extract_geojson_bbox(text);
Some(DatasetInfo {
format: DatasetFormat::GeoJson,
path: None, width: None,
height: None,
band_count: 0,
layer_count,
crs: None,
geotransform: None,
feature_count,
bounds,
})
}
fn count_geojson_features(text: &str) -> usize {
let mut count = 0usize;
let needle1 = "\"type\":\"Feature\"";
let needle2 = "\"type\": \"Feature\"";
let mut pos = 0;
while pos < text.len() {
if let Some(idx) = text[pos..].find(needle1) {
count += 1;
pos += idx + needle1.len();
} else if let Some(idx) = text[pos..].find(needle2) {
count += 1;
pos += idx + needle2.len();
} else {
break;
}
}
count
}
fn extract_geojson_bbox(text: &str) -> Option<crate::BoundingBox> {
let start = text.find("\"bbox\":")?;
let after_key = &text[start + 7..]; let bracket = after_key.find('[')? + 1;
let inner_start = bracket;
let inner_end = after_key.find(']')?;
if inner_end <= inner_start {
return None;
}
let inner = &after_key[inner_start..inner_end];
let nums: Vec<f64> = inner
.split(',')
.filter_map(|s| s.trim().parse::<f64>().ok())
.collect();
if nums.len() >= 4 {
crate::BoundingBox::new(nums[0], nums[1], nums[2], nums[3]).ok()
} else {
None
}
}
#[cfg(feature = "shapefile")]
pub(crate) fn extract_shapefile_info(path: &Path) -> Option<DatasetInfo> {
let base = path.with_extension("");
let reader = oxigdal_shapefile::ShapefileReader::open(&base).ok()?;
let header = reader.header();
let bbox = &header.bbox;
let feature_count = reader.index_entries().map(|entries| entries.len() as u64);
let bounds = crate::BoundingBox::new(bbox.x_min, bbox.y_min, bbox.x_max, bbox.y_max).ok();
let crs = reader.crs().map(str::to_string);
Some(DatasetInfo {
format: DatasetFormat::Shapefile,
path: None,
width: None,
height: None,
band_count: 0,
layer_count: 1,
crs,
geotransform: None,
feature_count,
bounds,
})
}
#[cfg(feature = "flatgeobuf")]
pub(crate) fn extract_flatgeobuf_info(path: &Path) -> Option<DatasetInfo> {
use std::io::BufReader;
let file = std::fs::File::open(path).ok()?;
let reader = oxigdal_flatgeobuf::FlatGeobufReader::new(BufReader::new(file)).ok()?;
let header = reader.header();
let feature_count = header.features_count;
let bounds = header.extent.and_then(|ext| {
crate::BoundingBox::new(ext[0], ext[1], ext[2], ext[3]).ok()
});
let crs = header
.crs
.as_ref()
.and_then(|c| c.organization_code)
.map(|code| format!("EPSG:{code}"));
Some(DatasetInfo {
format: DatasetFormat::FlatGeobuf,
path: None,
width: None,
height: None,
band_count: 0,
layer_count: 1,
crs,
geotransform: None,
feature_count,
bounds,
})
}
#[cfg(feature = "geoparquet")]
pub(crate) fn extract_geoparquet_info(path: &Path) -> Option<DatasetInfo> {
let reader = oxigdal_geoparquet::GeoParquetReader::open(path).ok()?;
let feature_count = {
let n = reader.num_rows();
if n >= 0 { Some(n as u64) } else { None }
};
let bounds = {
let meta = reader.metadata();
meta.columns
.get(&meta.primary_column)
.and_then(|col| col.bbox.as_ref())
.filter(|bbox| bbox.len() >= 4)
.and_then(|bbox| crate::BoundingBox::new(bbox[0], bbox[1], bbox[2], bbox[3]).ok())
};
Some(DatasetInfo {
format: DatasetFormat::GeoParquet,
path: None,
width: None,
height: None,
band_count: 0,
layer_count: 1,
crs: None,
geotransform: None,
feature_count,
bounds,
})
}
fn map_format_to_opened(format: DatasetFormat, info: DatasetInfo) -> OpenedDataset {
match format {
DatasetFormat::GeoTiff => OpenedDataset::GeoTiff(info),
DatasetFormat::GeoJson => OpenedDataset::GeoJson(info),
DatasetFormat::Shapefile => OpenedDataset::Shapefile(info),
DatasetFormat::GeoParquet => OpenedDataset::GeoParquet(info),
DatasetFormat::GeoPackage => OpenedDataset::GeoPackage(info),
DatasetFormat::NetCdf => OpenedDataset::NetCdf(info),
DatasetFormat::Hdf5 => OpenedDataset::Hdf5(info),
DatasetFormat::Zarr => OpenedDataset::Zarr(info),
DatasetFormat::Grib => OpenedDataset::Grib(info),
DatasetFormat::FlatGeobuf => OpenedDataset::FlatGeobuf(info),
DatasetFormat::Jpeg2000 => OpenedDataset::Jpeg2000(info),
DatasetFormat::Vrt => OpenedDataset::Vrt(info),
DatasetFormat::Stac => OpenedDataset::Stac(info),
DatasetFormat::PMTiles
| DatasetFormat::MBTiles
| DatasetFormat::Copc
| DatasetFormat::Terrain
| DatasetFormat::Unknown => OpenedDataset::Unknown(info),
}
}
impl DatasetFormat {
pub fn is_geopackage(path: &Path) -> bool {
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(str::to_lowercase)
.unwrap_or_default();
ext == "gpkg"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::magic::{HDF5_MAGIC, JP2_MAGIC};
use std::io::Write;
fn write_temp_file(name: &str, content: &[u8]) -> PathBuf {
let dir = std::env::temp_dir();
let path = dir.join(name);
let mut f = std::fs::File::create(&path).expect("create temp file");
f.write_all(content).expect("write temp file");
path
}
#[test]
fn test_cloud_s3_scheme_detected() {
let result = open("s3://my-bucket/data/world.tif");
assert!(result.is_ok(), "s3:// should succeed");
let ds = result.expect("s3 opened");
assert!(ds.is_cloud(), "should be cloud dataset");
if let OpenedDataset::Cloud { scheme, .. } = &ds {
assert_eq!(*scheme, CloudScheme::S3);
} else {
panic!("expected Cloud variant");
}
}
#[test]
fn test_cloud_gs_scheme_detected() {
let result = open("gs://bucket/raster.tif");
assert!(result.is_ok());
let ds = result.expect("gs opened");
assert!(ds.is_cloud());
if let OpenedDataset::Cloud { scheme, .. } = &ds {
assert_eq!(*scheme, CloudScheme::Gcs);
} else {
panic!("expected Cloud variant");
}
}
#[test]
fn test_cloud_az_scheme_detected() {
let result = open("az://container/layer.gpkg");
assert!(result.is_ok());
let ds = result.expect("az opened");
assert!(ds.is_cloud());
}
#[test]
fn test_cloud_http_scheme_detected() {
let result = open("https://example.com/layer.geojson");
assert!(result.is_ok());
let ds = result.expect("https opened");
assert!(ds.is_cloud());
if let OpenedDataset::Cloud { scheme, .. } = &ds {
assert_eq!(*scheme, CloudScheme::Http);
} else {
panic!("expected Cloud variant");
}
}
#[test]
fn test_cloud_guessed_format_from_extension() {
let result = open("s3://bucket/elevation.tif").expect("open");
if let OpenedDataset::Cloud { guessed_format, .. } = result {
assert_eq!(guessed_format, DatasetFormat::GeoTiff);
} else {
panic!("expected Cloud");
}
}
#[test]
fn test_open_nonexistent_file_returns_io_error() {
let result = open("/nonexistent/path/file.tif");
assert!(result.is_err(), "nonexistent file should error");
let err = result.expect_err("should be error");
assert!(
matches!(err, OxiGdalError::Io(IoError::NotFound { .. })),
"expected NotFound, got {err:?}"
);
}
#[test]
fn test_magic_tiff_little_endian() {
let bytes = [0x49u8, 0x49, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00];
let path = write_temp_file("test_magic_tiff_le.tif", &bytes);
let ds = open(&path).expect("open tiff le");
assert_eq!(ds.format(), DatasetFormat::GeoTiff);
assert!(ds.is_raster());
}
#[test]
fn test_magic_tiff_big_endian() {
let bytes = [0x4Du8, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00];
let path = write_temp_file("test_magic_tiff_be.tif", &bytes);
let ds = open(&path).expect("open tiff be");
assert_eq!(ds.format(), DatasetFormat::GeoTiff);
}
#[test]
fn test_magic_hdf5() {
let path = write_temp_file("test_magic_hdf5.h5", &HDF5_MAGIC);
let ds = open(&path).expect("open hdf5");
assert_eq!(ds.format(), DatasetFormat::Hdf5);
assert!(ds.is_raster());
}
#[test]
fn test_magic_netcdf() {
let bytes = [0x43u8, 0x44, 0x46, 0x01, 0x00, 0x00, 0x00, 0x00];
let path = write_temp_file("test_magic_netcdf.nc", &bytes);
let ds = open(&path).expect("open netcdf");
assert_eq!(ds.format(), DatasetFormat::NetCdf);
assert!(ds.is_raster());
}
#[test]
fn test_magic_jp2() {
let path = write_temp_file("test_magic_jp2.jp2", &JP2_MAGIC);
let ds = open(&path).expect("open jp2");
assert_eq!(ds.format(), DatasetFormat::Jpeg2000);
assert!(ds.is_raster());
}
#[test]
fn test_extension_geojson_fallback() {
let content = b"{}";
let path = write_temp_file("test_ext_fallback.geojson", content);
let ds = open(&path).expect("open geojson");
assert_eq!(ds.format(), DatasetFormat::GeoJson);
assert!(ds.is_vector());
}
#[test]
fn test_extension_shapefile_fallback() {
let content = b"\x00\x00\x27\x0A"; let path = write_temp_file("test_ext_shapefile.shp", content);
let ds = open(&path).expect("open shp");
assert_eq!(ds.format(), DatasetFormat::Shapefile);
assert!(ds.is_vector());
}
#[test]
fn test_extension_vrt_fallback() {
let content = b"<VRTDataset />";
let path = write_temp_file("test_ext_vrt.vrt", content);
let ds = open(&path).expect("open vrt");
assert_eq!(ds.format(), DatasetFormat::Vrt);
assert!(ds.is_raster());
}
#[test]
fn test_extension_grib_fallback() {
let content = b"GRIB";
let path = write_temp_file("test_ext_grib.grib", content);
let ds = open(&path).expect("open grib");
assert_eq!(ds.format(), DatasetFormat::Grib);
}
#[test]
fn test_opened_dataset_not_cloud_for_local() {
let content = b"{}";
let path = write_temp_file("test_not_cloud.geojson", content);
let ds = open(&path).expect("open");
assert!(!ds.is_cloud());
}
#[test]
fn test_opened_dataset_info_present_for_local() {
let content = b"{}";
let path = write_temp_file("test_info_present.geojson", content);
let ds = open(&path).expect("open");
assert!(ds.info().is_some(), "local file should have info");
}
#[test]
fn test_is_geopackage_extension_check() {
let path = Path::new("layer.gpkg");
assert!(DatasetFormat::is_geopackage(path));
let path2 = Path::new("world.tif");
assert!(!DatasetFormat::is_geopackage(path2));
}
#[test]
fn test_format_display_all_variants() {
assert_eq!(DatasetFormat::GeoTiff.to_string(), "GTiff");
assert_eq!(DatasetFormat::GeoJson.to_string(), "GeoJSON");
assert_eq!(DatasetFormat::Shapefile.to_string(), "ESRI Shapefile");
assert_eq!(DatasetFormat::Hdf5.to_string(), "HDF5");
assert_eq!(DatasetFormat::Vrt.to_string(), "VRT");
assert_eq!(DatasetFormat::Unknown.to_string(), "Unknown");
}
fn build_minimal_tiff_le(width: u32, height: u32, spp: u16) -> Vec<u8> {
let mut buf: Vec<u8> = vec![0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00];
let num_entries: u16 = 3;
buf.extend_from_slice(&num_entries.to_le_bytes());
buf.extend_from_slice(&256u16.to_le_bytes()); buf.extend_from_slice(&4u16.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&width.to_le_bytes()); buf.extend_from_slice(&257u16.to_le_bytes());
buf.extend_from_slice(&4u16.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&height.to_le_bytes());
buf.extend_from_slice(&277u16.to_le_bytes());
buf.extend_from_slice(&3u16.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&spp.to_le_bytes()); buf.extend_from_slice(&[0x00, 0x00]); buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
#[test]
fn test_tiff_metadata_extraction_width_height_bands() {
let tiff = build_minimal_tiff_le(1024, 768, 3);
let path = write_temp_file("test_meta_extract.tif", &tiff);
let ds = open(&path).expect("open tiff");
let info = ds.info().expect("should have info");
assert_eq!(info.format, DatasetFormat::GeoTiff);
assert_eq!(info.width, Some(1024));
assert_eq!(info.height, Some(768));
assert_eq!(info.band_count, 3);
}
#[test]
fn test_tiff_metadata_extraction_single_band() {
let tiff = build_minimal_tiff_le(512, 512, 1);
let path = write_temp_file("test_meta_extract_1band.tif", &tiff);
let ds = open(&path).expect("open tiff");
let info = ds.info().expect("info");
assert_eq!(info.width, Some(512));
assert_eq!(info.height, Some(512));
assert_eq!(info.band_count, 1);
}
#[test]
fn test_tiff_metadata_short_width() {
let mut buf: Vec<u8> = vec![0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00];
let num_entries: u16 = 2;
buf.extend_from_slice(&num_entries.to_le_bytes());
buf.extend_from_slice(&256u16.to_le_bytes());
buf.extend_from_slice(&3u16.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&640u16.to_le_bytes());
buf.extend_from_slice(&[0x00, 0x00]); buf.extend_from_slice(&257u16.to_le_bytes());
buf.extend_from_slice(&3u16.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&480u16.to_le_bytes());
buf.extend_from_slice(&[0x00, 0x00]);
buf.extend_from_slice(&0u32.to_le_bytes());
let path = write_temp_file("test_meta_short_width.tif", &buf);
let ds = open(&path).expect("open");
let info = ds.info().expect("info");
assert_eq!(info.width, Some(640));
assert_eq!(info.height, Some(480));
}
fn build_geotiff_with_transform(
width: u32,
height: u32,
origin_x: f64,
origin_y: f64,
scale_x: f64,
scale_y: f64,
) -> Vec<u8> {
let ps_offset: u32 = 200;
let tp_offset: u32 = 224;
let mut buf: Vec<u8> = vec![0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00];
let num_entries: u16 = 5;
buf.extend_from_slice(&num_entries.to_le_bytes());
buf.extend_from_slice(&256u16.to_le_bytes());
buf.extend_from_slice(&4u16.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&width.to_le_bytes());
buf.extend_from_slice(&257u16.to_le_bytes());
buf.extend_from_slice(&4u16.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&height.to_le_bytes());
buf.extend_from_slice(&277u16.to_le_bytes());
buf.extend_from_slice(&3u16.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&[0x00, 0x00]);
buf.extend_from_slice(&33550u16.to_le_bytes());
buf.extend_from_slice(&12u16.to_le_bytes()); buf.extend_from_slice(&3u32.to_le_bytes());
buf.extend_from_slice(&ps_offset.to_le_bytes());
buf.extend_from_slice(&33922u16.to_le_bytes());
buf.extend_from_slice(&12u16.to_le_bytes()); buf.extend_from_slice(&6u32.to_le_bytes());
buf.extend_from_slice(&tp_offset.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
while buf.len() < ps_offset as usize {
buf.push(0);
}
buf.extend_from_slice(&scale_x.to_le_bytes());
buf.extend_from_slice(&scale_y.to_le_bytes());
buf.extend_from_slice(&0.0_f64.to_le_bytes());
buf.extend_from_slice(&0.0_f64.to_le_bytes());
buf.extend_from_slice(&0.0_f64.to_le_bytes());
buf.extend_from_slice(&0.0_f64.to_le_bytes());
buf.extend_from_slice(&origin_x.to_le_bytes());
buf.extend_from_slice(&origin_y.to_le_bytes());
buf.extend_from_slice(&0.0_f64.to_le_bytes());
buf
}
#[test]
fn test_tiff_geotransform_extraction() {
let tiff = build_geotiff_with_transform(256, 256, -180.0, 90.0, 0.703125, 0.703125);
let path = write_temp_file("test_meta_geotransform.tif", &tiff);
let ds = open(&path).expect("open");
let info = ds.info().expect("info");
assert_eq!(info.width, Some(256));
assert_eq!(info.height, Some(256));
let gt = info.geotransform.expect("should have geotransform");
assert!(
(gt.origin_x - (-180.0)).abs() < 1e-10,
"origin_x: {}",
gt.origin_x
);
assert!(
(gt.origin_y - 90.0).abs() < 1e-10,
"origin_y: {}",
gt.origin_y
);
assert!(
(gt.pixel_width - 0.703125).abs() < 1e-10,
"pixel_width: {}",
gt.pixel_width
);
}
#[test]
fn test_tiff_big_endian_extraction() {
let mut buf: Vec<u8> = vec![0x4D, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08];
let num_entries: u16 = 2;
buf.extend_from_slice(&num_entries.to_be_bytes());
buf.extend_from_slice(&256u16.to_be_bytes());
buf.extend_from_slice(&4u16.to_be_bytes());
buf.extend_from_slice(&1u32.to_be_bytes());
buf.extend_from_slice(&800u32.to_be_bytes());
buf.extend_from_slice(&257u16.to_be_bytes());
buf.extend_from_slice(&4u16.to_be_bytes());
buf.extend_from_slice(&1u32.to_be_bytes());
buf.extend_from_slice(&600u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
let path = write_temp_file("test_meta_be.tif", &buf);
let ds = open(&path).expect("open");
let info = ds.info().expect("info");
assert_eq!(info.width, Some(800));
assert_eq!(info.height, Some(600));
}
#[test]
fn test_geojson_feature_collection_detected() {
let content = br#"{"type":"FeatureCollection","features":[{"type":"Feature","geometry":null,"properties":{}}]}"#;
let path = write_temp_file("test_meta_fc.geojson", content);
let ds = open(&path).expect("open");
let info = ds.info().expect("info");
assert_eq!(info.format, DatasetFormat::GeoJson);
assert_eq!(info.layer_count, 1);
}
#[test]
fn test_geojson_single_feature_no_collection() {
let content = br#"{"type":"Feature","geometry":null,"properties":{}}"#;
let path = write_temp_file("test_meta_single.geojson", content);
let ds = open(&path).expect("open");
let info = ds.info().expect("info");
assert_eq!(info.format, DatasetFormat::GeoJson);
assert_eq!(info.layer_count, 0);
}
}