lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

error.rs (7227B)


      1 use std::fmt;
      2 
      3 use crate::download::FetchFailurePhase;
      4 
      5 /// Failure returned by GeoNames configuration and lookup operations.
      6 #[derive(Clone, Debug, PartialEq, Eq)]
      7 #[non_exhaustive]
      8 pub enum Error {
      9     /// An asset version was empty or contained surrounding whitespace.
     10     InvalidAssetVersion,
     11     /// An asset file name was not one safe, relative path component.
     12     InvalidAssetFileName,
     13     /// An asset source or authority was empty or contained whitespace.
     14     InvalidAssetSource,
     15     /// An asset source did not use HTTPS with the declared authority.
     16     UntrustedAssetSource,
     17     /// An asset declared a zero byte size.
     18     InvalidAssetByteSize,
     19     /// The host-selected destination could not be used safely.
     20     UnsafeAssetDestination,
     21     /// Another acquisition currently owns the destination lock.
     22     AssetDestinationBusy,
     23     /// A filesystem operation failed.
     24     Io {
     25         /// Stable operation label without a host path.
     26         operation: &'static str,
     27         /// Portable I/O failure category.
     28         kind: std::io::ErrorKind,
     29     },
     30     /// The injected fetcher failed before producing a complete stream.
     31     Fetch {
     32         /// Stable acquisition phase.
     33         phase: FetchFailurePhase,
     34     },
     35     /// The acquired or inspected asset had the wrong length.
     36     AssetSizeMismatch {
     37         /// Declared asset length.
     38         expected: u64,
     39         /// Observed length, capped at `expected + 1` during acquisition.
     40         actual: u64,
     41     },
     42     /// The acquired or inspected asset had the wrong digest.
     43     AssetHashMismatch,
     44     /// The verified bytes were not a healthy SQLite database.
     45     InvalidDatabase,
     46     /// The database did not contain the governed GeoNames schema.
     47     InvalidDatabaseSchema,
     48     /// Exclusive access to the private database connection was unavailable.
     49     DatabaseConnectionUnavailable,
     50     /// A read-only database operation failed.
     51     DatabaseOperationFailed {
     52         /// Stable operation label without SQL or host paths.
     53         operation: &'static str,
     54     },
     55     /// A coordinate was non-finite or outside its geographic bounds.
     56     InvalidPoint,
     57     /// A query string was empty or contained surrounding whitespace.
     58     InvalidQueryText,
     59     /// A query limit was outside the supported range.
     60     InvalidQueryLimit,
     61     /// A reverse-query radius was non-finite or out of bounds.
     62     InvalidQueryRadius,
     63     /// A feature identifier could not be represented by the provider database.
     64     InvalidFeatureId,
     65     /// A locality-only option was applied to another query kind.
     66     QueryOptionNotApplicable,
     67 }
     68 
     69 impl fmt::Display for Error {
     70     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     71         match self {
     72             Self::InvalidAssetVersion => {
     73                 formatter.write_str("asset version must be non-empty and normalized")
     74             }
     75             Self::InvalidAssetFileName => {
     76                 formatter.write_str("asset file name must be one safe relative path component")
     77             }
     78             Self::InvalidAssetSource => {
     79                 formatter.write_str("asset source and authority must be non-empty and normalized")
     80             }
     81             Self::UntrustedAssetSource => {
     82                 formatter.write_str("asset source must use HTTPS and the declared authority")
     83             }
     84             Self::InvalidAssetByteSize => {
     85                 formatter.write_str("asset byte size must be greater than zero")
     86             }
     87             Self::UnsafeAssetDestination => formatter.write_str("asset destination is unsafe"),
     88             Self::AssetDestinationBusy => {
     89                 formatter.write_str("asset destination is already being acquired")
     90             }
     91             Self::Io { operation, kind } => write!(formatter, "{operation} failed: {kind}"),
     92             Self::Fetch { phase } => write!(formatter, "asset fetch failed during {phase}"),
     93             Self::AssetSizeMismatch { expected, actual } => write!(
     94                 formatter,
     95                 "asset size mismatch: expected {expected} bytes, observed {actual}"
     96             ),
     97             Self::AssetHashMismatch => {
     98                 formatter.write_str("asset SHA-256 does not match its specification")
     99             }
    100             Self::InvalidDatabase => formatter.write_str("asset is not a healthy SQLite database"),
    101             Self::InvalidDatabaseSchema => {
    102                 formatter.write_str("asset does not contain the governed GeoNames schema")
    103             }
    104             Self::DatabaseConnectionUnavailable => {
    105                 formatter.write_str("GeoNames database connection is unavailable")
    106             }
    107             Self::DatabaseOperationFailed { operation } => {
    108                 write!(formatter, "GeoNames database {operation} failed")
    109             }
    110             Self::InvalidPoint => {
    111                 formatter.write_str("point must contain finite, in-range coordinates")
    112             }
    113             Self::InvalidQueryText => {
    114                 formatter.write_str("query text must be non-empty and normalized")
    115             }
    116             Self::InvalidQueryLimit => {
    117                 formatter.write_str("query limit must be between 1 and 1000")
    118             }
    119             Self::InvalidQueryRadius => {
    120                 formatter.write_str("reverse-query radius must be greater than 0 and at most 10")
    121             }
    122             Self::InvalidFeatureId => {
    123                 formatter.write_str("feature identifier exceeds the provider database range")
    124             }
    125             Self::QueryOptionNotApplicable => {
    126                 formatter.write_str("query option is not applicable to this query kind")
    127             }
    128         }
    129     }
    130 }
    131 
    132 impl std::error::Error for Error {}
    133 
    134 #[cfg(test)]
    135 mod tests {
    136     use super::Error;
    137     use crate::download::FetchFailurePhase;
    138 
    139     #[test]
    140     fn every_public_error_has_a_secret_safe_stable_message() {
    141         let cases = [
    142             Error::InvalidAssetVersion,
    143             Error::InvalidAssetFileName,
    144             Error::InvalidAssetSource,
    145             Error::UntrustedAssetSource,
    146             Error::InvalidAssetByteSize,
    147             Error::UnsafeAssetDestination,
    148             Error::AssetDestinationBusy,
    149             Error::Io {
    150                 operation: "read asset",
    151                 kind: std::io::ErrorKind::PermissionDenied,
    152             },
    153             Error::Fetch {
    154                 phase: FetchFailurePhase::Connect,
    155             },
    156             Error::AssetSizeMismatch {
    157                 expected: 10,
    158                 actual: 9,
    159             },
    160             Error::AssetHashMismatch,
    161             Error::InvalidDatabase,
    162             Error::InvalidDatabaseSchema,
    163             Error::DatabaseConnectionUnavailable,
    164             Error::DatabaseOperationFailed { operation: "query" },
    165             Error::InvalidPoint,
    166             Error::InvalidQueryText,
    167             Error::InvalidQueryLimit,
    168             Error::InvalidQueryRadius,
    169             Error::InvalidFeatureId,
    170             Error::QueryOptionNotApplicable,
    171         ];
    172         for error in cases {
    173             let message = error.to_string();
    174             assert!(!message.is_empty());
    175             for forbidden in ["https://", "/tmp/", "SELECT ", "token="] {
    176                 assert!(!message.contains(forbidden));
    177             }
    178         }
    179     }
    180 }