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 }