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    /// # Example
179    ///
180    /// ```
181    /// use maxminddb::{Reader, geoip2};
182    /// use std::net::IpAddr;
183    ///
184    /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
185    /// let ip: IpAddr = "89.160.20.128".parse().unwrap();
186    ///
187    /// let result = reader.lookup(ip).unwrap();
188    /// if let Some(city) = result.decode::<geoip2::City>()? {
189    ///     println!("Found city data");
190    /// }
191    /// # Ok::<(), maxminddb::MaxMindDbError>(())
192    /// ```
193    pub fn decode<T>(&self) -> Result<Option<T>, MaxMindDbError>
194    where
195        T: Deserialize<'a>,
196    {
197        let Some(offset) = self.data_offset else {
198            return Ok(None);
199        };
200
201        let mut decoder = self.decoder(offset);
202        T::deserialize(&mut decoder).map(Some)
203    }
204
205    /// Decodes a value at a specific path within the record.
206    ///
207    /// Returns:
208    /// - `Ok(Some(T))` if the path exists and was successfully decoded
209    /// - `Ok(None)` if the path doesn't exist (key missing, index out of bounds)
210    /// - `Err(...)` if there's a type mismatch during navigation (e.g., `Key` on an array)
211    ///
212    /// If `has_data() == false`, returns `Ok(None)`.
213    ///
214    /// # Path Elements
215    ///
216    /// - `PathElement::Key("name")` - Navigate into a map by key
217    /// - `PathElement::Index(0)` - Navigate into an array by index (0 = first element)
218    /// - `PathElement::IndexFromEnd(0)` - Navigate from the end (0 = last element)
219    ///
220    /// # Example
221    ///
222    /// ```
223    /// use maxminddb::{path, Reader};
224    /// use std::net::IpAddr;
225    ///
226    /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
227    /// let ip: IpAddr = "89.160.20.128".parse().unwrap();
228    ///
229    /// let result = reader.lookup(ip).unwrap();
230    ///
231    /// // Navigate to country.iso_code
232    /// let iso_code: Option<String> = result
233    ///     .decode_path(&path!["country", "iso_code"])
234    ///     .unwrap();
235    ///
236    /// // Navigate to subdivisions[0].names.en
237    /// let subdiv_name: Option<String> = result
238    ///     .decode_path(&path!["subdivisions", 0, "names", "en"])
239    ///     .unwrap();
240    /// ```
241    pub fn decode_path<T>(&self, path: &[PathElement<'_>]) -> Result<Option<T>, MaxMindDbError>
242    where
243        T: Deserialize<'a>,
244    {
245        let Some(offset) = self.data_offset else {
246            return Ok(None);
247        };
248
249        let mut decoder = self.decoder(offset);
250
251        // Navigate through the path, tracking position for error context
252        for (i, element) in path.iter().enumerate() {
253            // Closure to add path context to errors during navigation.
254            // Shows path up to and including the current element where the error occurred.
255            let with_path = |e| add_path_context(e, &path[..=i]);
256
257            match *element {
258                PathElement::Key(key) => {
259                    let header_offset = decoder.offset();
260                    let (size, type_num) = decoder.consume_container_header().map_err(with_path)?;
261                    if type_num != TYPE_MAP {
262                        return Err(container_type_mismatch(
263                            type_num,
264                            header_offset,
265                            &path[..=i],
266                            format!("expected map for Key(\"{key}\"), got type {type_num}"),
267                        ));
268                    }
269
270                    let mut found = false;
271                    let key_bytes = key.as_bytes();
272                    for _ in 0..size {
273                        let k = decoder.read_str_as_bytes().map_err(with_path)?;
274                        if k == key_bytes {
275                            found = true;
276                            break;
277                        } else {
278                            decoder.skip_value().map_err(with_path)?;
279                        }
280                    }
281
282                    if !found {
283                        decoder.validate_skip_end().map_err(with_path)?;
284                        return Ok(None);
285                    }
286                }
287                PathElement::Index(idx) | PathElement::IndexFromEnd(idx) => {
288                    let header_offset = decoder.offset();
289                    let (size, type_num) = decoder.consume_container_header().map_err(with_path)?;
290                    if type_num != TYPE_ARRAY {
291                        let elem = match *element {
292                            PathElement::Index(i) => format!("Index({i})"),
293                            PathElement::IndexFromEnd(i) => format!("IndexFromEnd({i})"),
294                            PathElement::Key(_) => unreachable!(),
295                        };
296                        return Err(container_type_mismatch(
297                            type_num,
298                            header_offset,
299                            &path[..=i],
300                            format!("expected array for {elem}, got type {type_num}"),
301                        ));
302                    }
303
304                    if idx >= size {
305                        return Ok(None); // Out of bounds
306                    }
307
308                    let actual_idx = match *element {
309                        PathElement::Index(i) => i,
310                        PathElement::IndexFromEnd(i) => size - 1 - i,
311                        PathElement::Key(_) => unreachable!(),
312                    };
313
314                    // Skip to the target index
315                    for _ in 0..actual_idx {
316                        decoder.skip_value().map_err(with_path)?;
317                    }
318                }
319            }
320        }
321
322        // Decode the value at the current position
323        T::deserialize(&mut decoder)
324            .map(Some)
325            .map_err(|e| add_path_context(e, path))
326    }
327}
328
329#[cold]
330fn container_type_mismatch(
331    type_num: usize,
332    offset: usize,
333    path: &[PathElement<'_>],
334    message: String,
335) -> MaxMindDbError {
336    if type_num > usize::from(u8::MAX) {
337        MaxMindDbError::invalid_database_at(format!("unknown data type: {type_num}"), offset)
338    } else {
339        MaxMindDbError::decoding_at_path(message, offset, render_path(path))
340    }
341}
342
343/// Adds path context to a Decoding error if it doesn't already have one.
344fn add_path_context(err: MaxMindDbError, path: &[PathElement<'_>]) -> MaxMindDbError {
345    match err {
346        MaxMindDbError::Decoding {
347            message,
348            offset,
349            path: None,
350        } => MaxMindDbError::Decoding {
351            message,
352            offset,
353            path: Some(render_path(path)),
354        },
355        _ => err,
356    }
357}
358
359/// Renders path elements as a JSON-pointer-like string (e.g., "/city/names/0").
360fn render_path(path: &[PathElement<'_>]) -> String {
361    use std::fmt::Write;
362    let mut s = String::new();
363    for elem in path {
364        s.push('/');
365        match elem {
366            PathElement::Key(k) => s.push_str(k),
367            PathElement::Index(i) => write!(s, "{i}").unwrap(),
368            PathElement::IndexFromEnd(i) => write!(s, "-{}", (*i as u128) + 1).unwrap(),
369        }
370    }
371    s
372}
373
374/// A path element for navigating into nested data structures.
375///
376/// Used with [`LookupResult::decode_path()`] to selectively decode
377/// specific fields without parsing the entire record.
378///
379/// # Creating Path Elements
380///
381/// You can create path elements directly or use the [`path!`](crate::path) macro
382/// for a more convenient syntax:
383///
384/// ```
385/// use maxminddb::{path, PathElement};
386///
387/// // Direct construction
388/// let path = [PathElement::Key("country"), PathElement::Key("iso_code")];
389///
390/// // Using the macro - string literals become Keys, integers become Indexes
391/// let path = path!["country", "iso_code"];
392/// let path = path!["subdivisions", 0, "names"];  // Mixed keys and indexes
393/// let path = path!["array", -1];  // Negative indexes count from the end
394/// ```
395#[derive(Debug, Clone, PartialEq, Eq)]
396pub enum PathElement<'a> {
397    /// Navigate into a map by key.
398    Key(&'a str),
399    /// Navigate into an array by index (0-based from the start).
400    ///
401    /// - `Index(0)` - first element
402    /// - `Index(1)` - second element
403    Index(usize),
404    /// Navigate into an array by index from the end.
405    ///
406    /// - `IndexFromEnd(0)` - last element
407    /// - `IndexFromEnd(1)` - second-to-last element
408    IndexFromEnd(usize),
409}
410
411impl<'a> From<&'a str> for PathElement<'a> {
412    fn from(s: &'a str) -> Self {
413        PathElement::Key(s)
414    }
415}
416
417impl From<i32> for PathElement<'_> {
418    /// Converts an integer to a path element.
419    ///
420    /// - Non-negative values become `Index(n)`
421    /// - Negative values become `IndexFromEnd(-n - 1)`, so `-1` is the last element
422    fn from(n: i32) -> Self {
423        signed_index_to_path_element(n as isize)
424    }
425}
426
427impl From<usize> for PathElement<'_> {
428    fn from(n: usize) -> Self {
429        PathElement::Index(n)
430    }
431}
432
433impl From<isize> for PathElement<'_> {
434    /// Converts a signed integer to a path element.
435    ///
436    /// - Non-negative values become `Index(n)`
437    /// - Negative values become `IndexFromEnd(-n - 1)`, so `-1` is the last element
438    /// - `isize::MIN` saturates to `IndexFromEnd(usize::MAX)` because its
439    ///   absolute value is unrepresentable as `isize`
440    fn from(n: isize) -> Self {
441        signed_index_to_path_element(n)
442    }
443}
444
445fn signed_index_to_path_element<'a>(n: isize) -> PathElement<'a> {
446    if n >= 0 {
447        PathElement::Index(n as usize)
448    } else {
449        let index = n
450            .checked_neg()
451            .and_then(|n| n.checked_sub(1))
452            .map(|n| n as usize)
453            .unwrap_or(usize::MAX);
454        PathElement::IndexFromEnd(index)
455    }
456}
457
458/// Creates a path for use with [`LookupResult::decode_path()`](crate::LookupResult::decode_path).
459///
460/// This macro provides a convenient way to construct paths with mixed string keys
461/// and integer indexes.
462///
463/// # Syntax
464///
465/// - String literals become [`PathElement::Key`]
466/// - Non-negative integers become [`PathElement::Index`]
467/// - Negative integers become [`PathElement::IndexFromEnd`] (e.g., `-1` is the last element)
468///
469/// # Examples
470///
471/// ```
472/// use maxminddb::{Reader, path};
473/// use std::net::IpAddr;
474///
475/// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
476/// let ip: IpAddr = "89.160.20.128".parse().unwrap();
477/// let result = reader.lookup(ip).unwrap();
478///
479/// // Navigate to country.iso_code
480/// let iso_code: Option<String> = result.decode_path(&path!["country", "iso_code"]).unwrap();
481///
482/// // Navigate to subdivisions[0].names.en
483/// let subdiv: Option<String> = result.decode_path(&path!["subdivisions", 0, "names", "en"]).unwrap();
484/// ```
485///
486/// ```
487/// use maxminddb::{Reader, path};
488/// use std::net::IpAddr;
489///
490/// let reader = Reader::open_readfile("test-data/test-data/MaxMind-DB-test-decoder.mmdb").unwrap();
491/// let ip: IpAddr = "::1.1.1.0".parse().unwrap();
492/// let result = reader.lookup(ip).unwrap();
493///
494/// // Access the last element of an array
495/// let last: Option<u32> = result.decode_path(&path!["array", -1]).unwrap();
496/// assert_eq!(last, Some(3));
497///
498/// // Access the second-to-last element
499/// let second_to_last: Option<u32> = result.decode_path(&path!["array", -2]).unwrap();
500/// assert_eq!(second_to_last, Some(2));
501/// ```
502#[macro_export]
503macro_rules! path {
504    ($($elem:expr),* $(,)?) => {
505        [$($crate::PathElement::from($elem)),*]
506    };
507}
508
509/// Masks an IP address to its network address given a prefix length.
510fn mask_ip(ip: IpAddr, prefix: u8) -> IpAddr {
511    match ip {
512        IpAddr::V4(v4) => {
513            if prefix >= 32 {
514                IpAddr::V4(v4)
515            } else {
516                let int: u32 = v4.into();
517                let mask = if prefix == 0 {
518                    0
519                } else {
520                    !0u32 << (32 - prefix)
521                };
522                IpAddr::V4((int & mask).into())
523            }
524        }
525        IpAddr::V6(v6) => {
526            if prefix >= 128 {
527                IpAddr::V6(v6)
528            } else {
529                let int: u128 = v6.into();
530                let mask = if prefix == 0 {
531                    0
532                } else {
533                    !0u128 << (128 - prefix)
534                };
535                IpAddr::V6((int & mask).into())
536            }
537        }
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn test_mask_ipv4() {
547        let ip: IpAddr = "192.168.1.100".parse().unwrap();
548        assert_eq!(mask_ip(ip, 24), "192.168.1.0".parse::<IpAddr>().unwrap());
549        assert_eq!(mask_ip(ip, 16), "192.168.0.0".parse::<IpAddr>().unwrap());
550        assert_eq!(mask_ip(ip, 32), "192.168.1.100".parse::<IpAddr>().unwrap());
551        assert_eq!(mask_ip(ip, 0), "0.0.0.0".parse::<IpAddr>().unwrap());
552    }
553
554    #[test]
555    fn test_mask_ipv6() {
556        let ip: IpAddr = "2001:db8:85a3::8a2e:370:7334".parse().unwrap();
557        assert_eq!(
558            mask_ip(ip, 64),
559            "2001:db8:85a3::".parse::<IpAddr>().unwrap()
560        );
561        assert_eq!(mask_ip(ip, 32), "2001:db8::".parse::<IpAddr>().unwrap());
562    }
563
564    #[test]
565    fn test_path_element_debug() {
566        assert_eq!(format!("{:?}", PathElement::Key("test")), "Key(\"test\")");
567        assert_eq!(format!("{:?}", PathElement::Index(5)), "Index(5)");
568        assert_eq!(
569            format!("{:?}", PathElement::IndexFromEnd(0)),
570            "IndexFromEnd(0)"
571        );
572    }
573
574    #[test]
575    fn test_path_element_from_str() {
576        let elem: PathElement = "key".into();
577        assert_eq!(elem, PathElement::Key("key"));
578    }
579
580    #[test]
581    fn test_path_element_from_i32() {
582        // Positive values become Index
583        let elem: PathElement = PathElement::from(0i32);
584        assert_eq!(elem, PathElement::Index(0));
585
586        let elem: PathElement = PathElement::from(5i32);
587        assert_eq!(elem, PathElement::Index(5));
588
589        // Negative values become IndexFromEnd
590        // -1 → IndexFromEnd(0) (last element)
591        let elem: PathElement = PathElement::from(-1i32);
592        assert_eq!(elem, PathElement::IndexFromEnd(0));
593
594        // -2 → IndexFromEnd(1) (second-to-last)
595        let elem: PathElement = PathElement::from(-2i32);
596        assert_eq!(elem, PathElement::IndexFromEnd(1));
597
598        // -3 → IndexFromEnd(2)
599        let elem: PathElement = PathElement::from(-3i32);
600        assert_eq!(elem, PathElement::IndexFromEnd(2));
601    }
602
603    #[test]
604    fn test_path_element_from_usize() {
605        let elem: PathElement = PathElement::from(0usize);
606        assert_eq!(elem, PathElement::Index(0));
607
608        let elem: PathElement = PathElement::from(42usize);
609        assert_eq!(elem, PathElement::Index(42));
610    }
611
612    #[test]
613    fn test_path_element_from_isize() {
614        let elem: PathElement = PathElement::from(0isize);
615        assert_eq!(elem, PathElement::Index(0));
616
617        let elem: PathElement = PathElement::from(-1isize);
618        assert_eq!(elem, PathElement::IndexFromEnd(0));
619
620        let elem: PathElement = PathElement::from(isize::MIN);
621        assert_eq!(elem, PathElement::IndexFromEnd(usize::MAX));
622    }
623
624    #[test]
625    fn test_path_macro_keys_only() {
626        let p = path!["country", "iso_code"];
627        assert_eq!(p.len(), 2);
628        assert_eq!(p[0], PathElement::Key("country"));
629        assert_eq!(p[1], PathElement::Key("iso_code"));
630    }
631
632    #[test]
633    fn test_path_macro_mixed() {
634        let p = path!["subdivisions", 0, "names", "en"];
635        assert_eq!(p.len(), 4);
636        assert_eq!(p[0], PathElement::Key("subdivisions"));
637        assert_eq!(p[1], PathElement::Index(0));
638        assert_eq!(p[2], PathElement::Key("names"));
639        assert_eq!(p[3], PathElement::Key("en"));
640    }
641
642    #[test]
643    fn test_path_macro_negative_indexes() {
644        let p = path!["array", -1];
645        assert_eq!(p.len(), 2);
646        assert_eq!(p[0], PathElement::Key("array"));
647        assert_eq!(p[1], PathElement::IndexFromEnd(0)); // last element
648
649        let p = path!["data", -2, "value"];
650        assert_eq!(p[1], PathElement::IndexFromEnd(1)); // second-to-last
651    }
652
653    #[test]
654    fn test_path_macro_trailing_comma() {
655        let p = path!["a", "b",];
656        assert_eq!(p.len(), 2);
657    }
658
659    #[test]
660    fn test_path_macro_empty() {
661        let p: [PathElement; 0] = path![];
662        assert_eq!(p.len(), 0);
663    }
664
665    #[test]
666    fn test_render_path() {
667        assert_eq!(render_path(&[]), "");
668        assert_eq!(render_path(&[PathElement::Key("city")]), "/city");
669        assert_eq!(
670            render_path(&[PathElement::Key("city"), PathElement::Key("names")]),
671            "/city/names"
672        );
673        assert_eq!(
674            render_path(&[PathElement::Key("arr"), PathElement::Index(0)]),
675            "/arr/0"
676        );
677        assert_eq!(
678            render_path(&[PathElement::Key("arr"), PathElement::Index(42)]),
679            "/arr/42"
680        );
681        // IndexFromEnd(0) = last = -1, IndexFromEnd(1) = second-to-last = -2
682        assert_eq!(
683            render_path(&[PathElement::Key("arr"), PathElement::IndexFromEnd(0)]),
684            "/arr/-1"
685        );
686        assert_eq!(
687            render_path(&[PathElement::Key("arr"), PathElement::IndexFromEnd(1)]),
688            "/arr/-2"
689        );
690        assert_eq!(
691            render_path(&[PathElement::IndexFromEnd(isize::MAX as usize)]),
692            format!("/-{}", (isize::MAX as u128) + 1)
693        );
694        assert_eq!(
695            render_path(&[PathElement::IndexFromEnd(usize::MAX)]),
696            format!("/-{}", (usize::MAX as u128) + 1)
697        );
698    }
699
700    #[test]
701    fn test_decode_path_error_includes_path() {
702        use crate::Reader;
703
704        let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
705        let ip: IpAddr = "89.160.20.128".parse().unwrap();
706        let result = reader.lookup(ip).unwrap();
707
708        // Try to navigate with Index on a map (root is a map, not array)
709        let err = result
710            .decode_path::<String>(&[PathElement::Index(0)])
711            .unwrap_err();
712        let err_str = err.to_string();
713        assert!(
714            err_str.contains("path: /0"),
715            "error should include path context: {err_str}"
716        );
717        assert!(
718            err_str.contains("expected array"),
719            "error should mention expected type: {err_str}"
720        );
721
722        // Try to navigate deeper and fail at second element
723        let err = result
724            .decode_path::<String>(&[PathElement::Key("city"), PathElement::Index(0)])
725            .unwrap_err();
726        let err_str = err.to_string();
727        assert!(
728            err_str.contains("path: /city/0"),
729            "error should include full path to failure: {err_str}"
730        );
731    }
732
733    #[test]
734    fn test_overflowing_extended_navigation_type_is_invalid_database() {
735        let err = container_type_mismatch(
736            256,
737            7,
738            &[PathElement::Key("city")],
739            "unused mismatch".to_owned(),
740        );
741
742        assert!(matches!(err, MaxMindDbError::InvalidDatabase { .. }));
743        assert!(err.to_string().contains("unknown data type: 256"));
744    }
745}