1#![deny(missing_docs)]
14
15mod generated;
16
17use generated::{PointLookupResult, decode_variable_length_unsigned};
18use std::{
19 collections::HashMap, convert::TryInto, fs, io, path::Path,
20 string::FromUtf8Error,
21};
22
23#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct Location {
26 pub latitude: f32,
28 pub longitude: f32,
30}
31
32impl Location {
33 pub fn new(latitude: f32, longitude: f32) -> Location {
35 Location {
36 latitude,
37 longitude,
38 }
39 }
40}
41
42#[derive(Clone, Debug, Eq, PartialEq)]
44pub struct Zone {
45 pub polygon_id: u32,
47 pub meta_id: u32,
49 pub fields: HashMap<String, String>,
51}
52
53#[allow(missing_docs)]
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum ZoneMatchKind {
56 InZone,
57 InExcludedZone,
58 OnBorderVertex,
59 OnBorderSegment,
60}
61
62impl ZoneMatchKind {
63 fn from_point_lookup(r: PointLookupResult) -> Option<ZoneMatchKind> {
64 match r {
65 PointLookupResult::InZone => Some(ZoneMatchKind::InZone),
66 PointLookupResult::InExcludedZone => {
67 Some(ZoneMatchKind::InExcludedZone)
68 }
69 PointLookupResult::OnBorderVertex => {
70 Some(ZoneMatchKind::OnBorderVertex)
71 }
72 PointLookupResult::OnBorderSegment => {
73 Some(ZoneMatchKind::OnBorderSegment)
74 }
75 _ => None,
76 }
77 }
78}
79
80#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct ZoneMatch {
83 pub kind: ZoneMatchKind,
85 pub zone: Zone,
87}
88
89#[derive(Clone, Debug, PartialEq)]
91pub struct ZoneLookup {
92 pub matches: Vec<ZoneMatch>,
94 pub safezone: f32,
96}
97
98#[allow(missing_docs)]
99#[derive(Debug, thiserror::Error)]
100pub enum Error {
101 #[error("IO error")]
102 IoError(#[from] io::Error),
103 #[error("database header is truncated")]
104 TruncatedDatabase(usize),
105 #[error("invalid magic bytes")]
106 InvalidMagic([u8; 3]),
107 #[error("invalid version")]
108 InvalidVersion(u8),
109 #[error("invalid table type")]
110 InvalidTableType(u8),
111 #[error("invalid field name")]
112 InvalidFieldName(u8, StringParseError),
113 #[error("invalid notice")]
114 InvalidNotice(StringParseError),
115 #[error("invalid metadata offset")]
116 InvalidMetadataOffset,
117 #[error("invalid data offset")]
118 InvalidDataOffset,
119 #[error("invalid padding offset")]
122 InvalidPaddingOffset,
123 #[error("length mismatch")]
124 LengthMismatch(usize),
125}
126
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub enum TableType {
130 Country,
132 Timezone,
134}
135
136#[allow(missing_docs)]
137pub type Result<T> = std::result::Result<T, Error>;
138
139#[allow(missing_docs)]
140#[derive(Debug, thiserror::Error)]
141pub enum StringParseError {
142 #[error("encoding error")]
143 EncodingError,
144 #[error("invalid UTF-8")]
145 InvalidUtf8(#[from] FromUtf8Error),
146}
147
148fn parse_string(
149 db: &Database,
150 index: &mut u32,
151) -> std::result::Result<String, StringParseError> {
152 if let Some(bytes) = generated::parse_string(db, index) {
153 let string = String::from_utf8(bytes)?;
154 Ok(string)
155 } else {
156 Err(StringParseError::EncodingError)
157 }
158}
159
160pub struct Database {
162 bbox_offset: u32,
163 data_offset: u32,
164 mapping: Vec<u8>,
165 metadata_offset: u32,
166
167 pub field_names: Vec<String>,
169 pub notice: String,
171 pub precision: u8,
173 pub table_type: crate::TableType,
175 pub version: u8,
177}
178
179impl Database {
180 pub fn open<P: AsRef<Path>>(path: P) -> Result<Database> {
182 let mapping = fs::read(path)?;
183 Self::from_vec(mapping)
184 }
185
186 pub fn from_vec(mapping: Vec<u8>) -> Result<Database> {
188 let mut db = Database {
189 mapping,
190 notice: String::new(),
191 table_type: TableType::Country,
192 version: 0,
193 precision: 0,
194 field_names: Vec::new(),
195 bbox_offset: 0,
196 metadata_offset: 0,
197 data_offset: 0,
198 };
199 Self::parse_header(&mut db)?;
200 Ok(db)
201 }
202
203 fn parse_header(db: &mut Database) -> Result<()> {
204 if db.mapping.len() < 7 {
205 return Err(Error::TruncatedDatabase(db.mapping.len()));
206 }
207
208 let expected_magic = b"PLB";
209 let actual_magic = &db.mapping[0..3];
210 if actual_magic != expected_magic {
211 return Err(Error::InvalidMagic(
212 actual_magic.try_into().unwrap_or([0; 3]),
213 ));
214 }
215
216 let table_type = db.mapping[3];
217 db.version = db.mapping[4];
218 db.precision = db.mapping[5];
219 let num_fields = db.mapping[6];
220
221 if table_type == b'T' {
222 db.table_type = TableType::Timezone;
223 } else if table_type == b'C' {
224 db.table_type = TableType::Country;
225 } else {
226 return Err(Error::InvalidTableType(table_type));
227 }
228
229 if db.version >= 2 {
230 return Err(Error::InvalidVersion(db.version));
231 }
232
233 let mut index = 7;
235
236 db.field_names.reserve(num_fields as usize);
237 for field_index in 0..num_fields {
238 let name = parse_string(db, &mut index)
239 .map_err(|err| Error::InvalidFieldName(field_index, err))?;
240 db.field_names.push(name);
241 }
242
243 db.notice =
244 parse_string(db, &mut index).map_err(Error::InvalidNotice)?;
245
246 let mut tmp: u64 = 0;
248 if decode_variable_length_unsigned(db, &mut index, &mut tmp) == 0 {
249 return Err(Error::InvalidMetadataOffset);
250 }
251 db.metadata_offset = tmp as u32 + db.bbox_offset;
252
253 if decode_variable_length_unsigned(db, &mut index, &mut tmp) == 0 {
254 return Err(Error::InvalidDataOffset);
255 }
256 db.data_offset = tmp as u32 + db.metadata_offset;
257
258 if decode_variable_length_unsigned(db, &mut index, &mut tmp) == 0 {
259 return Err(Error::InvalidPaddingOffset);
260 }
261
262 db.bbox_offset += index;
264 db.metadata_offset += index;
265 db.data_offset += index;
266
267 let length = (tmp + db.data_offset as u64) as usize;
269 if length != db.mapping.len() {
270 return Err(Error::LengthMismatch(length));
271 }
272
273 Ok(())
274 }
275
276 pub fn simple_lookup(&self, location: Location) -> Option<String> {
281 let results = generated::lookup(self, location, None);
282
283 if let Some(result) = results.first() {
284 match self.table_type {
285 TableType::Country => result.zone.fields.get("Name"),
286 TableType::Timezone => {
287 if let Some(prefix) =
288 result.zone.fields.get("TimezoneIdPrefix")
289 {
290 if let Some(id) = result.zone.fields.get("TimezoneId") {
291 return Some(format!("{prefix}{id}"));
292 }
293 }
294 None
295 }
296 }
297 .cloned()
298 } else {
299 None
300 }
301 }
302
303 pub fn lookup(&self, location: Location) -> ZoneLookup {
305 let mut safezone: f32 = 0.0;
306 let results = generated::lookup(self, location, Some(&mut safezone));
307 let matches = results
308 .iter()
309 .map(|r| {
310 ZoneMatch {
311 kind: ZoneMatchKind::from_point_lookup(r.result)
314 .expect("invalid match kind"),
315 zone: r.zone.clone(),
316 }
317 })
318 .collect::<Vec<_>>();
319 ZoneLookup { matches, safezone }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn test_open() {
329 let db = Database::open("data/timezone21.bin").unwrap();
330 assert_eq!(db.bbox_offset, 288);
331 assert_eq!(db.metadata_offset, 31803);
332 assert_eq!(db.data_offset, 40825);
333 assert_eq!(db.notice, "Contains data from Natural Earth, placed in the Public Domain. Contains information from https://github.com/evansiroky/timezone-boundary-builder, which is made available here under the Open Database License \\(ODbL\\).".to_string());
334 assert_eq!(db.table_type, TableType::Timezone);
335 assert_eq!(db.precision, 21);
336 assert_eq!(
337 db.field_names,
338 vec![
339 "TimezoneIdPrefix".to_string(),
340 "TimezoneId".to_string(),
341 "CountryAlpha2".to_string(),
342 "CountryName".to_string(),
343 ]
344 );
345 }
346
347 #[test]
348 fn test_simple_lookup() {
349 let db = Database::open("data/timezone21.bin").unwrap();
350 assert_eq!(
352 db.simple_lookup(Location::new(39.9042, 116.4074)).unwrap(),
353 "Asia/Shanghai"
354 );
355 assert_eq!(
357 db.simple_lookup(Location::new(-34.6037, -58.3816)).unwrap(),
358 "America/Argentina/Buenos_Aires"
359 );
360 assert_eq!(
362 db.simple_lookup(Location::new(-35.2809, 149.13)).unwrap(),
363 "Australia/Sydney"
364 );
365 assert_eq!(
367 db.simple_lookup(Location::new(40.7128, -74.0060)).unwrap(),
368 "America/New_York"
369 );
370 }
371}