use std::fmt;
use geopackage_core::extensions::ExtensionSupport;
use crate::index::SpatialIndexAudit;
use crate::{ExtensionScope, GeoPackage, GpkgVersion, Result, SpatialIndexStatus, table_exists};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Advisory,
Warning,
Error,
}
impl Severity {
pub fn as_str(self) -> &'static str {
match self {
Self::Advisory => "advisory",
Self::Warning => "warning",
Self::Error => "error",
}
}
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Finding {
LegacyApplicationId {
version: GpkgVersion,
application_id: u32,
},
MissingContentsTable {
table_name: String,
},
TableNameCaseMismatch {
declared: String,
actual: String,
},
RemovedExtension {
extension_name: String,
table_name: Option<String>,
},
UnrecognisedExtension {
extension_name: String,
table_name: Option<String>,
scope: ExtensionScope,
},
SpatialIndexOutOfStep {
table_name: String,
audit: SpatialIndexAudit,
},
LegacySpatialIndexTriggers {
table_name: String,
},
NoSpatialIndex {
table_name: String,
},
TilePyramidInconsistent {
table_name: String,
detail: String,
},
DanglingMetadataReference {
md_id: i64,
},
MissingMappingTable {
mapping_table_name: String,
},
NonConformantRelationName {
relation_name: String,
},
}
impl Finding {
pub fn severity(&self) -> Severity {
match self {
Self::MissingContentsTable { .. }
| Self::SpatialIndexOutOfStep { .. }
| Self::DanglingMetadataReference { .. }
| Self::MissingMappingTable { .. } => Severity::Error,
Self::LegacyApplicationId { .. }
| Self::TableNameCaseMismatch { .. }
| Self::RemovedExtension { .. }
| Self::UnrecognisedExtension { .. }
| Self::LegacySpatialIndexTriggers { .. }
| Self::TilePyramidInconsistent { .. }
| Self::NonConformantRelationName { .. } => Severity::Warning,
Self::NoSpatialIndex { .. } => Severity::Advisory,
}
}
pub fn table_name(&self) -> Option<&str> {
match self {
Self::MissingContentsTable { table_name }
| Self::SpatialIndexOutOfStep { table_name, .. }
| Self::LegacySpatialIndexTriggers { table_name }
| Self::NoSpatialIndex { table_name }
| Self::TilePyramidInconsistent { table_name, .. } => Some(table_name),
Self::TableNameCaseMismatch { declared, .. } => Some(declared),
Self::RemovedExtension { table_name, .. }
| Self::UnrecognisedExtension { table_name, .. } => table_name.as_deref(),
Self::MissingMappingTable {
mapping_table_name, ..
} => Some(mapping_table_name),
Self::LegacyApplicationId { .. }
| Self::DanglingMetadataReference { .. }
| Self::NonConformantRelationName { .. } => None,
}
}
pub fn repair(&self) -> Option<&'static str> {
match self {
Self::SpatialIndexOutOfStep { .. } => {
Some("rebuild the index with Layer::rebuild_spatial_index")
}
Self::LegacySpatialIndexTriggers { .. } => {
Some("upgrade the trigger set with Layer::repair_spatial_index")
}
Self::NoSpatialIndex { .. } => Some("build one with Layer::create_spatial_index"),
Self::LegacyApplicationId { .. } => {
Some("rewriting the file through this crate stamps the current application_id")
}
Self::MissingContentsTable { .. } => {
Some("delete the gpkg_contents row, or restore the table it names")
}
Self::TableNameCaseMismatch { .. }
| Self::RemovedExtension { .. }
| Self::UnrecognisedExtension { .. }
| Self::TilePyramidInconsistent { .. }
| Self::DanglingMetadataReference { .. }
| Self::MissingMappingTable { .. }
| Self::NonConformantRelationName { .. } => None,
}
}
}
impl fmt::Display for Finding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LegacyApplicationId {
version,
application_id,
} => {
let bytes = application_id.to_be_bytes();
let tag = String::from_utf8_lossy(&bytes);
write!(
f,
"file declares the GeoPackage {version} application_id {tag:?}, which predates 1.2"
)
}
Self::MissingContentsTable { table_name } => {
write!(
f,
"gpkg_contents names table {table_name:?}, which is not in the file"
)
}
Self::TableNameCaseMismatch { declared, actual } => {
write!(
f,
"gpkg_contents says {declared:?} but the table is {actual:?}: they differ only in case"
)
}
Self::RemovedExtension {
extension_name,
table_name,
} => {
write!(
f,
"extension {extension_name:?}{} was removed from the standard in 2016",
On(table_name)
)
}
Self::UnrecognisedExtension {
extension_name,
table_name,
scope,
} => {
write!(
f,
"extension {extension_name:?}{} is not one this crate recognises (scope {})",
On(table_name),
scope.as_str()
)
}
Self::SpatialIndexOutOfStep { table_name, audit } => {
write!(
f,
"spatial index on {table_name:?} is out of step: {} indexable rows, {} entries, {} missing, {} stale, {} not covering their geometry",
audit.indexable, audit.entries, audit.missing, audit.extra, audit.not_covering
)
}
Self::LegacySpatialIndexTriggers { table_name } => {
write!(
f,
"spatial index on {table_name:?} is maintained by a pre-1.4 or mixed trigger set"
)
}
Self::NoSpatialIndex { table_name } => {
write!(f, "feature table {table_name:?} has no spatial index")
}
Self::TilePyramidInconsistent { table_name, detail } => {
write!(
f,
"tile pyramid {table_name:?} breaks the tile matrix rules: {detail}"
)
}
Self::DanglingMetadataReference { md_id } => {
write!(
f,
"gpkg_metadata_reference points at metadata id {md_id}, which is not there"
)
}
Self::MissingMappingTable { mapping_table_name } => {
write!(
f,
"relationship names mapping table {mapping_table_name:?}, which is not in the file"
)
}
Self::NonConformantRelationName { relation_name } => {
write!(
f,
"relation_name {relation_name:?} is not one Requirement 8 accepts"
)
}
}
}
}
struct On<'a>(&'a Option<String>);
impl fmt::Display for On<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Some(table) => write!(f, " on {table:?}"),
None => Ok(()),
}
}
}
impl GeoPackage {
pub fn validate(&self) -> Result<Vec<Finding>> {
let mut findings = Vec::new();
self.validate_container(&mut findings)?;
self.validate_extensions(&mut findings)?;
self.validate_spatial_indexes(&mut findings)?;
self.validate_tile_pyramids(&mut findings)?;
self.validate_metadata(&mut findings)?;
self.validate_relations(&mut findings)?;
findings.sort_by_key(|finding| std::cmp::Reverse(finding.severity()));
Ok(findings)
}
fn validate_container(&self, findings: &mut Vec<Finding>) -> Result<()> {
let conn = self.connection();
let application_id =
u32::try_from(conn.query_row("PRAGMA application_id", [], |row| row.get::<_, i64>(0))?)
.unwrap_or_default();
let user_version =
u32::try_from(conn.query_row("PRAGMA user_version", [], |row| row.get::<_, i64>(0))?)
.unwrap_or_default();
if let Some(version @ (GpkgVersion::V1_0 | GpkgVersion::V1_1)) =
GpkgVersion::from_pragmas(application_id, user_version)
{
findings.push(Finding::LegacyApplicationId {
version,
application_id,
});
}
for entry in self.contents()? {
if table_exists(conn, &entry.table_name)? {
continue;
}
let actual: Option<String> = conn
.query_row(
"SELECT name FROM sqlite_master WHERE type IN ('table', 'view') \
AND name = ?1 COLLATE NOCASE",
[&entry.table_name],
|row| row.get(0),
)
.ok();
match actual {
Some(actual) => findings.push(Finding::TableNameCaseMismatch {
declared: entry.table_name,
actual,
}),
None => findings.push(Finding::MissingContentsTable {
table_name: entry.table_name,
}),
}
}
Ok(())
}
fn validate_extensions(&self, findings: &mut Vec<Finding>) -> Result<()> {
for row in self.extensions()? {
match row.support() {
ExtensionSupport::Removed => findings.push(Finding::RemovedExtension {
extension_name: row.name,
table_name: row.table_name,
}),
ExtensionSupport::Unrecognised => findings.push(Finding::UnrecognisedExtension {
extension_name: row.name,
table_name: row.table_name,
scope: row.scope,
}),
_ => {}
}
}
Ok(())
}
fn validate_spatial_indexes(&self, findings: &mut Vec<Finding>) -> Result<()> {
for layer in self.layers()? {
let table_name = layer.table_name().to_owned();
match layer.spatial_index_status()? {
SpatialIndexStatus::Absent => {
findings.push(Finding::NoSpatialIndex { table_name });
}
SpatialIndexStatus::Legacy => {
findings.push(Finding::LegacySpatialIndexTriggers { table_name });
}
SpatialIndexStatus::Current | SpatialIndexStatus::Stale => {
let audit = layer.audit_spatial_index()?;
if !audit.is_consistent() {
findings.push(Finding::SpatialIndexOutOfStep { table_name, audit });
}
}
}
}
Ok(())
}
fn validate_tile_pyramids(&self, findings: &mut Vec<Finding>) -> Result<()> {
for pyramid in self.tile_pyramids()? {
if let Err(error) = pyramid.validate() {
findings.push(Finding::TilePyramidInconsistent {
table_name: pyramid.table_name().to_owned(),
detail: error.to_string(),
});
}
}
Ok(())
}
fn validate_metadata(&self, findings: &mut Vec<Finding>) -> Result<()> {
let records = self.metadata()?;
if records.is_empty() {
return Ok(());
}
let known: Vec<i64> = records.iter().map(|record| record.id).collect();
for reference in self.metadata_references()? {
for id in std::iter::once(reference.md_file_id).chain(reference.md_parent_id) {
if !known.contains(&id) {
findings.push(Finding::DanglingMetadataReference { md_id: id });
}
}
}
Ok(())
}
fn validate_relations(&self, findings: &mut Vec<Finding>) -> Result<()> {
let conn = self.connection();
for relation in self.relations()? {
if !table_exists(conn, &relation.mapping_table_name)? {
findings.push(Finding::MissingMappingTable {
mapping_table_name: relation.mapping_table_name.clone(),
});
}
if !relation.relation_name.is_conformant() {
findings.push(Finding::NonConformantRelationName {
relation_name: relation.relation_name.as_string(),
});
}
}
Ok(())
}
}