Skip to main content

maxminddb/
result.rs

1//! Lookup result types for deferred decoding.
2//!
3//! This module provides `LookupResult`, which enables lazy decoding of
4//! MaxMind DB records. Instead of immediately deserializing data, you
5//! get a lightweight handle that can be decoded later or navigated
6//! selectively via paths.
7
8use std::net::IpAddr;
9
10use ipnetwork::IpNetwork;
11use serde::Deserialize;
12
13use crate::decoder::{TYPE_ARRAY, TYPE_MAP};
14use crate::error::MaxMindDbError;
15use crate::reader::Reader;
16
17/// The result of looking up an IP address in a MaxMind DB.
18///
19/// This is a lightweight handle (~40 bytes) that stores the lookup result
20/// without immediately decoding the data. You can:
21///
22/// - Check if data exists with [`has_data()`](Self::has_data)
23/// - Get the network containing the IP with [`network()`](Self::network)
24/// - Decode the full record with [`decode()`](Self::decode)
25/// - Decode a specific path with [`decode_path()`](Self::decode_path)
26///
27/// # Example
28///
29/// ```
30/// use maxminddb::{geoip2, path, Reader};
31/// use std::net::IpAddr;
32///
33/// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
34/// let ip: IpAddr = "89.160.20.128".parse().unwrap();
35///
36/// let result = reader.lookup(ip).unwrap();
37///
38/// if result.has_data() {
39///     // Full decode
40///     let city: geoip2::City = result.decode().unwrap().unwrap();
41///
42///     // Or selective decode via path
43///     let country_code: Option<String> = result
44///         .decode_path(&path!["country", "iso_code"])
45///         .unwrap();
46///     println!("Country: {:?}", country_code);
47/// }
48/// ```
49#[derive(Debug, Clone, Copy)]
50pub struct LookupResult<'a, S: AsRef<[u8]>> {
51    reader: &'a Reader<S>,
52    /// Offset into the data section, or None if not found.
53    data_offset: Option<usize>,
54    prefix_len: u8,
55    ip: IpAddr,
56    source: LookupSource,
57    network_kind: NetworkKind,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub(crate) enum LookupSource {
62    Lookup,
63    Iter,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub(crate) enum NetworkKind {
68    V4,
69    V6,
70    V4InV6Subtree,
71}
72
73impl<'a, S: AsRef<[u8]>> LookupResult<'a, S> {
74    #[inline]
75    fn decoder(&self, offset: usize) -> super::decoder::Decoder<'a> {
76        let buf = &self.reader.buf.as_ref()[self.reader.pointer_base..];
77        super::decoder::Decoder::new_with_limit(buf, offset, self.reader.data_section_len)
78    }
79
80    /// Creates a new LookupResult for a found IP.
81    pub(crate) fn new_found(
82        reader: &'a Reader<S>,
83        data_offset: usize,
84        prefix_len: u8,
85        ip: IpAddr,
86        source: LookupSource,
87        network_kind: NetworkKind,
88    ) -> Self {
89        LookupResult {
90            reader,
91            data_offset: Some(data_offset),
92            prefix_len,
93            ip,
94            source,
95            network_kind,
96        }
97    }
98
99    /// Creates a new LookupResult for an IP not in the database.
100    pub(crate) fn new_not_found(
101        reader: &'a Reader<S>,
102        prefix_len: u8,
103        ip: IpAddr,
104        source: LookupSource,
105        network_kind: NetworkKind,
106    ) -> Self {
107        LookupResult {
108            reader,
109            data_offset: None,
110            prefix_len,
111            ip,
112            source,
113            network_kind,
114        }
115    }
116
117    /// Returns true if the database contains data for this IP address.
118    ///
119    /// Note that `false` means the database has no data for this IP,
120    /// which is different from an error during lookup.
121    #[inline]
122    pub fn has_data(&self) -> bool {
123        self.data_offset.is_some()
124    }
125
126    /// Returns the network containing the looked-up IP address.
127    ///
128    /// This is the most specific network in the database that contains
129    /// the IP, regardless of whether data was found.
130    ///
131    /// The returned network preserves the IP version of the original lookup:
132    /// - IPv4 lookups return IPv4 networks (unless the match occurs before the
133    ///   IPv4 subtree begins, see below)
134    /// - IPv6 lookups return IPv6 networks (including IPv4-mapped addresses)
135    ///
136    /// Special case: If an IPv4 address is looked up in an IPv6 database but
137    /// the matching record is above the IPv4 subtree (e.g., a database with
138    /// no IPv4 subtree), an IPv6 network is returned since there's no valid
139    /// IPv4 representation.
140    pub fn network(&self) -> Result<IpNetwork, MaxMindDbError> {
141        let (ip, prefix) = match (self.source, self.network_kind, self.ip) {
142            (_, NetworkKind::V4, IpAddr::V4(v4)) => (IpAddr::V4(v4), self.prefix_len),
143            (_, NetworkKind::V4InV6Subtree, IpAddr::V4(v4)) => (
144                IpAddr::V4(v4),
145                self.prefix_len - self.reader.ipv4_start_bit_depth as u8,
146            ),
147            (LookupSource::Lookup, NetworkKind::V6, IpAddr::V4(_)) => {
148                use std::net::Ipv6Addr;
149                (IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.prefix_len)
150            }
151            (_, NetworkKind::V6, IpAddr::V6(v6)) => (IpAddr::V6(v6), self.prefix_len),
152            (_, _, ip) => unreachable!("unexpected lookup result state for network: {ip:?}"),
153        };
154
155        // Mask the IP to the network address
156        let network_ip = mask_ip(ip, prefix);
157        IpNetwork::new(network_ip, prefix).map_err(MaxMindDbError::InvalidNetwork)
158    }
159
160    /// Returns the data section offset if found, for use as a cache key.
161    ///
162    /// Multiple IP addresses often point to the same data record. This
163    /// offset can be used to deduplicate decoding or cache results.
164    ///
165    /// Returns `None` if the IP was not found.
166    #[inline]
167    pub fn offset(&self) -> Option<usize> {
168        self.data_offset
169    }
170
171    /// Decodes the full record into the specified type.
172    ///
173    /// Returns:
174    /// - `Ok(Some(T))` if found and successfully decoded
175    /// - `Ok(None)` if the IP was not found in the database
176    /// - `Err(...)` if decoding fails
177    ///
178    /// Any operation that enters an MMDB map or array has an expansion budget
179    /// of 65,536 logical values and 2 MiB of string and bytes payload. Dynamic
180    /// `deserialize_any`, enum, and raw-string-helper entry points activate the
181    /// budget before the value's type is known. Only scalar values requested
182    /// directly through a typed scalar entry point avoid this bookkeeping. The
183    /// decoder reserves a container's declared children before Serde can
184    /// allocate for them, repeated pointer targets are charged on every
185    /// expansion, and ignored fields do not expand pointer targets. Exceeding
186    /// either decoder-wide operation limit returns
187    /// [`MaxMindDbError::ResourceLimit`] rather than treating the database as
188    /// necessarily corrupt.
189    ///
190    /// Concrete-schema identifiers get a 32-byte allowance per logical value
191    /// before using the 2 MiB payload counter, whether they are encoded inline
192    /// or behind a pointer. The logical-value limit bounds all such allowances
193    /// to another 2 MiB. Thus, after an operation activates its budget,
194    /// expanded string and byte payload remains bounded to at most 4 MiB even
195    /// for custom identifier visitors. A scalar-only typed decode remains
196    /// limited only by the MMDB format's maximum encoded payload size.
197    ///
198    /// These general limits do not replace tighter bounds implied by an
199    /// application's schema. A collection with a small semantic maximum should
200    /// enforce it in its `Deserialize` implementation or a Serde
201    /// `deserialize_with` visitor, before allocating or consuming its elements.
202    /// The built-in [`crate::geoip2::City`] and [`crate::geoip2::Enterprise`]
203    /// schemas cap their subdivision lists at
204    /// [`crate::geoip2::MAX_SUBDIVISIONS`] in every Serde format. An otherwise
205    /// valid oversized MMDB list that reaches the schema visitor returns
206    /// [`MaxMindDbError::Decoding`]; malformed data and decoder-wide limits may
207    /// fail earlier with their corresponding error variants.
208    /// Custom deserializers that bypass Serde's map and sequence entry points
209    /// remain responsible for bounding their own traversal over untrusted data.
210    ///
211    /// # Example
212    ///
213    /// ```
214    /// use maxminddb::{Reader, geoip2};
215    /// use std::net::IpAddr;
216    ///
217    /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
218    /// let ip: IpAddr = "89.160.20.128".parse().unwrap();
219    ///
220    /// let result = reader.lookup(ip).unwrap();
221    /// if let Some(city) = result.decode::<geoip2::City>()? {
222    ///     println!("Found city data");
223    /// }
224    /// # Ok::<(), maxminddb::MaxMindDbError>(())
225    /// ```
226    #[inline]
227    pub fn decode<T>(&self) -> Result<Option<T>, MaxMindDbError>
228    where
229        T: Deserialize<'a>,
230    {
231        let Some(offset) = self.data_offset else {
232            return Ok(None);
233        };
234
235        let mut decoder = self.decoder(offset);
236        T::deserialize(&mut decoder)
237            .map(Some)
238            .map_err(|error| error.with_invalid_database_offset_base(self.reader.pointer_base))
239    }
240
241    /// Decodes a value at a specific path within the record.
242    ///
243    /// Returns:
244    /// - `Ok(Some(T))` if the path exists and was successfully decoded
245    /// - `Ok(None)` if the path doesn't exist (key missing, index out of bounds)
246    /// - `Err(...)` if there's a type mismatch during navigation (e.g., `Key` on an array)
247    ///
248    /// If `has_data() == false`, returns `Ok(None)`.
249    /// Path traversal does not expand skipped pointer targets. Navigation and
250    /// the selected value share the container and payload budgets described by
251    /// [`decode()`](Self::decode); resource-limit errors include the path reached
252    /// when the limit was detected.
253    ///
254    /// # Path Elements
255    ///
256    /// - `PathElement::Key("name")` - Navigate into a map by key
257    /// - `PathElement::Index(0)` - Navigate into an array by index (0 = first element)
258    /// - `PathElement::IndexFromEnd(0)` - Navigate from the end (0 = last element)
259    ///
260    /// # Example
261    ///
262    /// ```
263    /// use maxminddb::{path, Reader};
264    /// use std::net::IpAddr;
265    ///
266    /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
267    /// let ip: IpAddr = "89.160.20.128".parse().unwrap();
268    ///
269    /// let result = reader.lookup(ip).unwrap();
270    ///
271    /// // Navigate to country.iso_code
272    /// let iso_code: Option<String> = result
273    ///     .decode_path(&path!["country", "iso_code"])
274    ///     .unwrap();
275    ///
276    /// // Navigate to subdivisions[0].names.en
277    /// let subdiv_name: Option<String> = result
278    ///     .decode_path(&path!["subdivisions", 0, "names", "en"])
279    ///     .unwrap();
280    /// ```
281    pub fn decode_path<T>(&self, path: &[PathElement<'_>]) -> Result<Option<T>, MaxMindDbError>
282    where
283        T: Deserialize<'a>,
284    {
285        self.decode_path_relative(path)
286            .map_err(|error| error.with_invalid_database_offset_base(self.reader.pointer_base))
287    }
288
289    #[inline]
290    fn decode_path_relative<T>(&self, path: &[PathElement<'_>]) -> Result<Option<T>, MaxMindDbError>
291    where
292        T: Deserialize<'a>,
293    {
294        let Some(offset) = self.data_offset else {
295            return Ok(None);
296        };
297
298        let mut decoder = self.decoder(offset);
299
300        // Navigate through the path, tracking position for error context
301        for (i, element) in path.iter().enumerate() {
302            // Closure to add path context to errors during navigation.
303            // Shows path up to and including the current element where the error occurred.
304            let with_path =
305                |e: crate::decoder::DecoderError| add_path_context(e.into(), &path[..=i]);
306
307            match *element {
308                PathElement::Key(key) => {
309                    let header_offset = decoder.offset();
310                    let (size, type_num) = decoder.consume_container_header().map_err(with_path)?;
311                    if type_num != TYPE_MAP {
312                        return Err(container_type_mismatch(
313                            type_num,
314                            header_offset,
315                            &path[..=i],
316                            format!("expected map for Key(\"{key}\"), got type {type_num}"),
317                        ));
318                    }
319
320                    let mut found = false;
321                    let key_bytes = key.as_bytes();
322                    for _ in 0..size {
323                        let k = decoder.read_str_as_bytes().map_err(with_path)?;
324                        if k == key_bytes {
325                            found = true;
326                            break;
327                        } else {
328                            decoder.skip_value().map_err(with_path)?;
329                        }
330                    }
331
332                    if !found {
333                        decoder.validate_skip_end().map_err(with_path)?;
334                        return Ok(None);
335                    }
336                }
337                PathElement::Index(idx) | PathElement::IndexFromEnd(idx) => {
338                    let header_offset = decoder.offset();
339                    let (size, type_num) = decoder.consume_container_header().map_err(with_path)?;
340                    if type_num != TYPE_ARRAY {
341                        let elem = match *element {
342                            PathElement::Index(i) => format!("Index({i})"),
343                            PathElement::IndexFromEnd(i) => format!("IndexFromEnd({i})"),
344                            PathElement::Key(_) => unreachable!(),
345                        };
346                        return Err(container_type_mismatch(
347                            type_num,
348                            header_offset,
349                            &path[..=i],
350                            format!("expected array for {elem}, got type {type_num}"),
351                        ));
352                    }
353
354                    if idx >= size {
355                        return Ok(None); // Out of bounds
356                    }
357
358                    let actual_idx = match *element {
359                        PathElement::Index(i) => i,
360                        PathElement::IndexFromEnd(i) => size - 1 - i,
361                        PathElement::Key(_) => unreachable!(),
362                    };
363
364                    // Skip to the target index
365                    for _ in 0..actual_idx {
366                        decoder.skip_value().map_err(with_path)?;
367                    }
368                }
369            }
370        }
371
372        // Decode the value at the current position
373        T::deserialize(&mut decoder)
374            .map(Some)
375            .map_err(|error| add_path_context(error.into(), path))
376    }
377}
378
379#[cold]
380fn container_type_mismatch(
381    type_num: usize,
382    offset: usize,
383    path: &[PathElement<'_>],
384    message: String,
385) -> MaxMindDbError {
386    if type_num > usize::from(u8::MAX) {
387        MaxMindDbError::invalid_database_at(format!("unknown data type: {type_num}"), offset)
388    } else {
389        MaxMindDbError::decoding_at_path(message, offset, render_path(path))
390    }
391}
392
393/// Adds path context to a decoding or resource-limit error if it does not
394/// already have one.
395fn add_path_context(err: MaxMindDbError, path: &[PathElement<'_>]) -> MaxMindDbError {
396    match err {
397        MaxMindDbError::Decoding {
398            message,
399            offset,
400            path: None,
401        } => MaxMindDbError::Decoding {
402            message,
403            offset,
404            path: Some(render_path(path)),
405        },
406        MaxMindDbError::ResourceLimit {
407            message,
408            offset,
409            path: None,
410        } => MaxMindDbError::ResourceLimit {
411            message,
412            offset,
413            path: Some(render_path(path)),
414        },
415        _ => err,
416    }
417}
418
419/// Renders path elements as a JSON-pointer-like string (e.g., "/city/names/0").
420fn render_path(path: &[PathElement<'_>]) -> String {
421    use std::fmt::Write;
422    let mut s = String::new();
423    for elem in path {
424        s.push('/');
425        match elem {
426            PathElement::Key(k) => s.push_str(k),
427            PathElement::Index(i) => write!(s, "{i}").unwrap(),
428            PathElement::IndexFromEnd(i) => write!(s, "-{}", (*i as u128) + 1).unwrap(),
429        }
430    }
431    s
432}
433
434/// A path element for navigating into nested data structures.
435///
436/// Used with [`LookupResult::decode_path()`] to selectively decode
437/// specific fields without parsing the entire record.
438///
439/// # Creating Path Elements
440///
441/// You can create path elements directly or use the [`path!`](crate::path) macro
442/// for a more convenient syntax:
443///
444/// ```
445/// use maxminddb::{path, PathElement};
446///
447/// // Direct construction
448/// let path = [PathElement::Key("country"), PathElement::Key("iso_code")];
449///
450/// // Using the macro - string literals become Keys, integers become Indexes
451/// let path = path!["country", "iso_code"];
452/// let path = path!["subdivisions", 0, "names"];  // Mixed keys and indexes
453/// let path = path!["array", -1];  // Negative indexes count from the end
454/// ```
455#[derive(Debug, Clone, PartialEq, Eq)]
456pub enum PathElement<'a> {
457    /// Navigate into a map by key.
458    Key(&'a str),
459    /// Navigate into an array by index (0-based from the start).
460    ///
461    /// - `Index(0)` - first element
462    /// - `Index(1)` - second element
463    Index(usize),
464    /// Navigate into an array by index from the end.
465    ///
466    /// - `IndexFromEnd(0)` - last element
467    /// - `IndexFromEnd(1)` - second-to-last element
468    IndexFromEnd(usize),
469}
470
471impl<'a> From<&'a str> for PathElement<'a> {
472    fn from(s: &'a str) -> Self {
473        PathElement::Key(s)
474    }
475}
476
477impl From<i32> for PathElement<'_> {
478    /// Converts an integer to a path element.
479    ///
480    /// - Non-negative values become `Index(n)`
481    /// - Negative values become `IndexFromEnd(-n - 1)`, so `-1` is the last element
482    fn from(n: i32) -> Self {
483        signed_index_to_path_element(n as isize)
484    }
485}
486
487impl From<usize> for PathElement<'_> {
488    fn from(n: usize) -> Self {
489        PathElement::Index(n)
490    }
491}
492
493impl From<isize> for PathElement<'_> {
494    /// Converts a signed integer to a path element.
495    ///
496    /// - Non-negative values become `Index(n)`
497    /// - Negative values become `IndexFromEnd(-n - 1)`, so `-1` is the last element
498    /// - `isize::MIN` saturates to `IndexFromEnd(usize::MAX)` because its
499    ///   absolute value is unrepresentable as `isize`
500    fn from(n: isize) -> Self {
501        signed_index_to_path_element(n)
502    }
503}
504
505fn signed_index_to_path_element<'a>(n: isize) -> PathElement<'a> {
506    if n >= 0 {
507        PathElement::Index(n as usize)
508    } else {
509        let index = n
510            .checked_neg()
511            .and_then(|n| n.checked_sub(1))
512            .map(|n| n as usize)
513            .unwrap_or(usize::MAX);
514        PathElement::IndexFromEnd(index)
515    }
516}
517
518/// Creates a path for use with [`LookupResult::decode_path()`](crate::LookupResult::decode_path).
519///
520/// This macro provides a convenient way to construct paths with mixed string keys
521/// and integer indexes.
522///
523/// # Syntax
524///
525/// - String literals become [`PathElement::Key`]
526/// - Non-negative integers become [`PathElement::Index`]
527/// - Negative integers become [`PathElement::IndexFromEnd`] (e.g., `-1` is the last element)
528///
529/// # Examples
530///
531/// ```
532/// use maxminddb::{Reader, path};
533/// use std::net::IpAddr;
534///
535/// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
536/// let ip: IpAddr = "89.160.20.128".parse().unwrap();
537/// let result = reader.lookup(ip).unwrap();
538///
539/// // Navigate to country.iso_code
540/// let iso_code: Option<String> = result.decode_path(&path!["country", "iso_code"]).unwrap();
541///
542/// // Navigate to subdivisions[0].names.en
543/// let subdiv: Option<String> = result.decode_path(&path!["subdivisions", 0, "names", "en"]).unwrap();
544/// ```
545///
546/// ```
547/// use maxminddb::{Reader, path};
548/// use std::net::IpAddr;
549///
550/// let reader = Reader::open_readfile("test-data/test-data/MaxMind-DB-test-decoder.mmdb").unwrap();
551/// let ip: IpAddr = "::1.1.1.0".parse().unwrap();
552/// let result = reader.lookup(ip).unwrap();
553///
554/// // Access the last element of an array
555/// let last: Option<u32> = result.decode_path(&path!["array", -1]).unwrap();
556/// assert_eq!(last, Some(3));
557///
558/// // Access the second-to-last element
559/// let second_to_last: Option<u32> = result.decode_path(&path!["array", -2]).unwrap();
560/// assert_eq!(second_to_last, Some(2));
561/// ```
562#[macro_export]
563macro_rules! path {
564    ($($elem:expr),* $(,)?) => {
565        [$($crate::PathElement::from($elem)),*]
566    };
567}
568
569/// Masks an IP address to its network address given a prefix length.
570fn mask_ip(ip: IpAddr, prefix: u8) -> IpAddr {
571    match ip {
572        IpAddr::V4(v4) => {
573            if prefix >= 32 {
574                IpAddr::V4(v4)
575            } else {
576                let int: u32 = v4.into();
577                let mask = if prefix == 0 {
578                    0
579                } else {
580                    !0u32 << (32 - prefix)
581                };
582                IpAddr::V4((int & mask).into())
583            }
584        }
585        IpAddr::V6(v6) => {
586            if prefix >= 128 {
587                IpAddr::V6(v6)
588            } else {
589                let int: u128 = v6.into();
590                let mask = if prefix == 0 {
591                    0
592                } else {
593                    !0u128 << (128 - prefix)
594                };
595                IpAddr::V6((int & mask).into())
596            }
597        }
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604
605    #[test]
606    fn test_mask_ipv4() {
607        let ip: IpAddr = "192.168.1.100".parse().unwrap();
608        assert_eq!(mask_ip(ip, 24), "192.168.1.0".parse::<IpAddr>().unwrap());
609        assert_eq!(mask_ip(ip, 16), "192.168.0.0".parse::<IpAddr>().unwrap());
610        assert_eq!(mask_ip(ip, 32), "192.168.1.100".parse::<IpAddr>().unwrap());
611        assert_eq!(mask_ip(ip, 0), "0.0.0.0".parse::<IpAddr>().unwrap());
612    }
613
614    #[test]
615    fn test_mask_ipv6() {
616        let ip: IpAddr = "2001:db8:85a3::8a2e:370:7334".parse().unwrap();
617        assert_eq!(
618            mask_ip(ip, 64),
619            "2001:db8:85a3::".parse::<IpAddr>().unwrap()
620        );
621        assert_eq!(mask_ip(ip, 32), "2001:db8::".parse::<IpAddr>().unwrap());
622    }
623
624    #[test]
625    fn test_path_element_debug() {
626        assert_eq!(format!("{:?}", PathElement::Key("test")), "Key(\"test\")");
627        assert_eq!(format!("{:?}", PathElement::Index(5)), "Index(5)");
628        assert_eq!(
629            format!("{:?}", PathElement::IndexFromEnd(0)),
630            "IndexFromEnd(0)"
631        );
632    }
633
634    #[test]
635    fn test_path_element_from_str() {
636        let elem: PathElement = "key".into();
637        assert_eq!(elem, PathElement::Key("key"));
638    }
639
640    #[test]
641    fn test_path_element_from_i32() {
642        // Positive values become Index
643        let elem: PathElement = PathElement::from(0i32);
644        assert_eq!(elem, PathElement::Index(0));
645
646        let elem: PathElement = PathElement::from(5i32);
647        assert_eq!(elem, PathElement::Index(5));
648
649        // Negative values become IndexFromEnd
650        // -1 → IndexFromEnd(0) (last element)
651        let elem: PathElement = PathElement::from(-1i32);
652        assert_eq!(elem, PathElement::IndexFromEnd(0));
653
654        // -2 → IndexFromEnd(1) (second-to-last)
655        let elem: PathElement = PathElement::from(-2i32);
656        assert_eq!(elem, PathElement::IndexFromEnd(1));
657
658        // -3 → IndexFromEnd(2)
659        let elem: PathElement = PathElement::from(-3i32);
660        assert_eq!(elem, PathElement::IndexFromEnd(2));
661    }
662
663    #[test]
664    fn test_path_element_from_usize() {
665        let elem: PathElement = PathElement::from(0usize);
666        assert_eq!(elem, PathElement::Index(0));
667
668        let elem: PathElement = PathElement::from(42usize);
669        assert_eq!(elem, PathElement::Index(42));
670    }
671
672    #[test]
673    fn test_path_element_from_isize() {
674        let elem: PathElement = PathElement::from(0isize);
675        assert_eq!(elem, PathElement::Index(0));
676
677        let elem: PathElement = PathElement::from(-1isize);
678        assert_eq!(elem, PathElement::IndexFromEnd(0));
679
680        let elem: PathElement = PathElement::from(isize::MIN);
681        assert_eq!(elem, PathElement::IndexFromEnd(usize::MAX));
682    }
683
684    #[test]
685    fn test_path_macro_keys_only() {
686        let p = path!["country", "iso_code"];
687        assert_eq!(p.len(), 2);
688        assert_eq!(p[0], PathElement::Key("country"));
689        assert_eq!(p[1], PathElement::Key("iso_code"));
690    }
691
692    #[test]
693    fn test_path_macro_mixed() {
694        let p = path!["subdivisions", 0, "names", "en"];
695        assert_eq!(p.len(), 4);
696        assert_eq!(p[0], PathElement::Key("subdivisions"));
697        assert_eq!(p[1], PathElement::Index(0));
698        assert_eq!(p[2], PathElement::Key("names"));
699        assert_eq!(p[3], PathElement::Key("en"));
700    }
701
702    #[test]
703    fn test_path_macro_negative_indexes() {
704        let p = path!["array", -1];
705        assert_eq!(p.len(), 2);
706        assert_eq!(p[0], PathElement::Key("array"));
707        assert_eq!(p[1], PathElement::IndexFromEnd(0)); // last element
708
709        let p = path!["data", -2, "value"];
710        assert_eq!(p[1], PathElement::IndexFromEnd(1)); // second-to-last
711    }
712
713    #[test]
714    fn test_path_macro_trailing_comma() {
715        let p = path!["a", "b",];
716        assert_eq!(p.len(), 2);
717    }
718
719    #[test]
720    fn test_path_macro_empty() {
721        let p: [PathElement; 0] = path![];
722        assert_eq!(p.len(), 0);
723    }
724
725    #[test]
726    fn test_render_path() {
727        assert_eq!(render_path(&[]), "");
728        assert_eq!(render_path(&[PathElement::Key("city")]), "/city");
729        assert_eq!(
730            render_path(&[PathElement::Key("city"), PathElement::Key("names")]),
731            "/city/names"
732        );
733        assert_eq!(
734            render_path(&[PathElement::Key("arr"), PathElement::Index(0)]),
735            "/arr/0"
736        );
737        assert_eq!(
738            render_path(&[PathElement::Key("arr"), PathElement::Index(42)]),
739            "/arr/42"
740        );
741        // IndexFromEnd(0) = last = -1, IndexFromEnd(1) = second-to-last = -2
742        assert_eq!(
743            render_path(&[PathElement::Key("arr"), PathElement::IndexFromEnd(0)]),
744            "/arr/-1"
745        );
746        assert_eq!(
747            render_path(&[PathElement::Key("arr"), PathElement::IndexFromEnd(1)]),
748            "/arr/-2"
749        );
750        assert_eq!(
751            render_path(&[PathElement::IndexFromEnd(isize::MAX as usize)]),
752            format!("/-{}", (isize::MAX as u128) + 1)
753        );
754        assert_eq!(
755            render_path(&[PathElement::IndexFromEnd(usize::MAX)]),
756            format!("/-{}", (usize::MAX as u128) + 1)
757        );
758    }
759
760    #[test]
761    fn test_decode_path_error_includes_path() {
762        use crate::Reader;
763
764        let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
765        let ip: IpAddr = "89.160.20.128".parse().unwrap();
766        let result = reader.lookup(ip).unwrap();
767
768        // Try to navigate with Index on a map (root is a map, not array)
769        let err = result
770            .decode_path::<String>(&[PathElement::Index(0)])
771            .unwrap_err();
772        let err_str = err.to_string();
773        assert!(
774            err_str.contains("path: /0"),
775            "error should include path context: {err_str}"
776        );
777        assert!(
778            err_str.contains("expected array"),
779            "error should mention expected type: {err_str}"
780        );
781
782        // Try to navigate deeper and fail at second element
783        let err = result
784            .decode_path::<String>(&[PathElement::Key("city"), PathElement::Index(0)])
785            .unwrap_err();
786        let err_str = err.to_string();
787        assert!(
788            err_str.contains("path: /city/0"),
789            "error should include full path to failure: {err_str}"
790        );
791    }
792
793    #[test]
794    fn test_overflowing_extended_navigation_type_is_invalid_database() {
795        let err = container_type_mismatch(
796            256,
797            7,
798            &[PathElement::Key("city")],
799            "unused mismatch".to_owned(),
800        );
801
802        assert!(matches!(err, MaxMindDbError::InvalidDatabase { .. }));
803        assert!(err.to_string().contains("unknown data type: 256"));
804    }
805
806    #[test]
807    fn test_resource_limit_error_includes_path() {
808        let err = add_path_context(
809            MaxMindDbError::resource_limit_at("too many values", 7),
810            &[PathElement::Key("subdivisions")],
811        );
812
813        assert!(matches!(
814            err,
815            MaxMindDbError::ResourceLimit {
816                path: Some(ref path),
817                ..
818            } if path == "/subdivisions"
819        ));
820    }
821}