maxminddb/reader.rs
1//! MaxMind DB reader implementation.
2
3use std::collections::HashSet;
4use std::fs;
5use std::net::IpAddr;
6use std::path::Path;
7
8use ipnetwork::IpNetwork;
9use serde::Deserialize;
10
11#[cfg(feature = "mmap")]
12pub use memmap2::Mmap;
13#[cfg(feature = "mmap")]
14use memmap2::MmapOptions;
15#[cfg(feature = "mmap")]
16use std::fs::File;
17
18use crate::decoder;
19use crate::error::MaxMindDbError;
20use crate::metadata::Metadata;
21use crate::result::LookupResult;
22use crate::within::{IpInt, Within, WithinNode, WithinOptions};
23
24/// Size of the data section separator (16 zero bytes).
25const DATA_SECTION_SEPARATOR_SIZE: usize = 16;
26
27/// A reader for the MaxMind DB format. The lifetime `'data` is tied to the
28/// lifetime of the underlying buffer holding the contents of the database file.
29///
30/// The `Reader` supports both file-based and memory-mapped access to MaxMind
31/// DB files, including GeoIP2 and GeoLite2 databases.
32///
33/// # Features
34///
35/// - **`mmap`**: Enable memory-mapped file access for better performance
36/// - **`simdutf8`**: Use SIMD-accelerated UTF-8 validation (faster string
37/// decoding)
38/// - **`unsafe-str-decode`**: Skip UTF-8 validation entirely (unsafe, but
39/// ~20% faster)
40pub struct Reader<S: AsRef<[u8]>> {
41 pub(crate) buf: S,
42 /// Database metadata.
43 pub metadata: Metadata,
44 pub(crate) ipv4_start: usize,
45 /// Bit depth at which ipv4_start was found (0-96). Used to calculate
46 /// correct prefix lengths for IPv4 lookups in IPv6 databases.
47 pub(crate) ipv4_start_bit_depth: usize,
48 pub(crate) pointer_base: usize,
49}
50
51impl<S: AsRef<[u8]>> std::fmt::Debug for Reader<S> {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.debug_struct("Reader")
54 .field("buf_len", &self.buf.as_ref().len())
55 .field("metadata", &self.metadata)
56 .field("ipv4_start", &self.ipv4_start)
57 .field("ipv4_start_bit_depth", &self.ipv4_start_bit_depth)
58 .field("pointer_base", &self.pointer_base)
59 .finish_non_exhaustive()
60 }
61}
62
63#[cfg(feature = "mmap")]
64impl Reader<Mmap> {
65 /// Open a MaxMind DB database file by memory mapping it.
66 ///
67 /// # Safety
68 ///
69 /// The caller must ensure that the database file is not modified or
70 /// truncated while the `Reader` exists. Modifying or truncating the
71 /// file while it is memory-mapped will result in undefined behavior.
72 ///
73 /// # Example
74 ///
75 /// ```
76 /// # #[cfg(feature = "mmap")]
77 /// # {
78 /// // SAFETY: The database file will not be modified while the reader exists.
79 /// let reader = unsafe {
80 /// maxminddb::Reader::open_mmap("test-data/test-data/GeoIP2-City-Test.mmdb")
81 /// }.unwrap();
82 /// # }
83 /// ```
84 pub unsafe fn open_mmap<P: AsRef<Path>>(database: P) -> Result<Reader<Mmap>, MaxMindDbError> {
85 let file_read = File::open(database)?;
86 let mmap = MmapOptions::new()
87 .map(&file_read)
88 .map_err(MaxMindDbError::Mmap)?;
89 Reader::from_source(mmap)
90 }
91}
92
93impl Reader<Vec<u8>> {
94 /// Open a MaxMind DB database file by loading it into memory.
95 ///
96 /// # Example
97 ///
98 /// ```
99 /// let reader = maxminddb::Reader::open_readfile(
100 /// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
101 /// ```
102 pub fn open_readfile<P: AsRef<Path>>(database: P) -> Result<Reader<Vec<u8>>, MaxMindDbError> {
103 let buf: Vec<u8> = fs::read(&database)?; // IO error converted via #[from]
104 Reader::from_source(buf)
105 }
106}
107
108impl<'de, S: AsRef<[u8]>> Reader<S> {
109 /// Open a MaxMind DB database from anything that implements AsRef<[u8]>
110 ///
111 /// # Example
112 ///
113 /// ```
114 /// use std::fs;
115 /// let buf = fs::read("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
116 /// let reader = maxminddb::Reader::from_source(buf).unwrap();
117 /// ```
118 pub fn from_source(buf: S) -> Result<Reader<S>, MaxMindDbError> {
119 let data_section_separator_size = 16;
120
121 let metadata_start = find_metadata_start(buf.as_ref())?;
122 let mut type_decoder = decoder::Decoder::new(&buf.as_ref()[metadata_start..], 0);
123 let metadata = Metadata::deserialize(&mut type_decoder)?;
124
125 let search_tree_size = (metadata.node_count as usize) * (metadata.record_size as usize) / 4;
126
127 let mut reader = Reader {
128 buf,
129 pointer_base: search_tree_size + data_section_separator_size,
130 metadata,
131 ipv4_start: 0,
132 ipv4_start_bit_depth: 0,
133 };
134 let (ipv4_start, ipv4_start_bit_depth) = reader.find_ipv4_start()?;
135 reader.ipv4_start = ipv4_start;
136 reader.ipv4_start_bit_depth = ipv4_start_bit_depth;
137
138 Ok(reader)
139 }
140
141 /// Lookup an IP address in the database.
142 ///
143 /// Returns a [`LookupResult`] that can be used to:
144 /// - Check if data exists with [`has_data()`](LookupResult::has_data)
145 /// - Get the network containing the IP with [`network()`](LookupResult::network)
146 /// - Decode the full record with [`decode()`](LookupResult::decode)
147 /// - Decode a specific path with [`decode_path()`](LookupResult::decode_path)
148 /// - Get a low-level decoder with [`decoder()`](LookupResult::decoder)
149 ///
150 /// # Examples
151 ///
152 /// Basic city lookup:
153 /// ```
154 /// # use maxminddb::geoip2;
155 /// # use std::net::IpAddr;
156 /// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
157 /// let reader = maxminddb::Reader::open_readfile(
158 /// "test-data/test-data/GeoIP2-City-Test.mmdb")?;
159 ///
160 /// let ip: IpAddr = "89.160.20.128".parse().unwrap();
161 /// let result = reader.lookup(ip)?;
162 ///
163 /// if let Some(city) = result.decode::<geoip2::City>()? {
164 /// // Access nested structs directly - no Option unwrapping needed
165 /// if let Some(name) = city.city.names.english {
166 /// println!("City: {}", name);
167 /// }
168 /// } else {
169 /// println!("No data found for IP {}", ip);
170 /// }
171 /// # Ok(())
172 /// # }
173 /// ```
174 ///
175 /// Selective field access:
176 /// ```
177 /// # use maxminddb::{Reader, PathElement};
178 /// # use std::net::IpAddr;
179 /// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
180 /// let reader = Reader::open_readfile(
181 /// "test-data/test-data/GeoIP2-City-Test.mmdb")?;
182 /// let ip: IpAddr = "89.160.20.128".parse().unwrap();
183 ///
184 /// let result = reader.lookup(ip)?;
185 /// let country_code: Option<String> = result.decode_path(&[
186 /// PathElement::Key("country"),
187 /// PathElement::Key("iso_code"),
188 /// ])?;
189 ///
190 /// println!("Country: {:?}", country_code);
191 /// # Ok(())
192 /// # }
193 /// ```
194 pub fn lookup(&'de self, address: IpAddr) -> Result<LookupResult<'de, S>, MaxMindDbError> {
195 // Check for IPv6 address in IPv4-only database
196 if matches!(address, IpAddr::V6(_)) && self.metadata.ip_version == 4 {
197 return Err(MaxMindDbError::invalid_input(
198 "cannot look up IPv6 address in IPv4-only database",
199 ));
200 }
201
202 let ip_int = IpInt::new(address);
203 let (pointer, prefix_len) = self.find_address_in_tree(&ip_int)?;
204
205 // For IPv4 addresses in IPv6 databases, adjust prefix_len to reflect
206 // the actual bit depth in the tree. The ipv4_start_bit_depth tells us
207 // how deep in the IPv6 tree we were when we found the IPv4 subtree.
208 let prefix_len = if matches!(address, IpAddr::V4(_)) && self.metadata.ip_version == 6 {
209 self.ipv4_start_bit_depth + prefix_len
210 } else {
211 prefix_len
212 };
213
214 if pointer == 0 {
215 // IP not found in database
216 Ok(LookupResult::new_not_found(self, prefix_len as u8, address))
217 } else {
218 // Resolve the pointer to a data offset
219 let data_offset = self.resolve_data_pointer(pointer)?;
220 Ok(LookupResult::new_found(
221 self,
222 data_offset,
223 prefix_len as u8,
224 address,
225 ))
226 }
227 }
228
229 /// Iterate over all networks in the database.
230 ///
231 /// This is a convenience method equivalent to calling [`within()`](Self::within)
232 /// with `0.0.0.0/0` for IPv4-only databases or `::/0` for IPv6 databases.
233 ///
234 /// # Arguments
235 ///
236 /// * `options` - Controls which networks are yielded. Use [`Default::default()`]
237 /// for standard behavior.
238 ///
239 /// # Examples
240 ///
241 /// Iterate over all networks with default options:
242 /// ```
243 /// use maxminddb::{geoip2, Reader};
244 ///
245 /// let reader = Reader::open_readfile(
246 /// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
247 ///
248 /// let mut count = 0;
249 /// for result in reader.networks(Default::default()).unwrap() {
250 /// let lookup = result.unwrap();
251 /// count += 1;
252 /// if count >= 10 { break; }
253 /// }
254 /// ```
255 pub fn networks(&'de self, options: WithinOptions) -> Result<Within<'de, S>, MaxMindDbError> {
256 let cidr = if self.metadata.ip_version == 6 {
257 IpNetwork::V6("::/0".parse().unwrap())
258 } else {
259 IpNetwork::V4("0.0.0.0/0".parse().unwrap())
260 };
261 self.within(cidr, options)
262 }
263
264 /// Iterate over IP networks within a CIDR range.
265 ///
266 /// Returns an iterator that yields [`LookupResult`] for each network in the
267 /// database that falls within the specified CIDR range.
268 ///
269 /// # Arguments
270 ///
271 /// * `cidr` - The CIDR range to iterate over.
272 /// * `options` - Controls which networks are yielded. Use [`Default::default()`]
273 /// for standard behavior (skip aliases, skip networks without data, include
274 /// empty values).
275 ///
276 /// # Examples
277 ///
278 /// Iterate over all IPv4 networks:
279 /// ```
280 /// use ipnetwork::IpNetwork;
281 /// use maxminddb::{geoip2, Reader};
282 ///
283 /// let reader = Reader::open_readfile(
284 /// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
285 ///
286 /// let ipv4_all = IpNetwork::V4("0.0.0.0/0".parse().unwrap());
287 /// let mut count = 0;
288 /// for result in reader.within(ipv4_all, Default::default()).unwrap() {
289 /// let lookup = result.unwrap();
290 /// let network = lookup.network().unwrap();
291 /// let city: geoip2::City = lookup.decode().unwrap().unwrap();
292 /// let city_name = city.city.names.english;
293 /// println!("Network: {}, City: {:?}", network, city_name);
294 /// count += 1;
295 /// if count >= 10 { break; } // Limit output for example
296 /// }
297 /// ```
298 ///
299 /// Search within a specific subnet:
300 /// ```
301 /// use ipnetwork::IpNetwork;
302 /// use maxminddb::{geoip2, Reader};
303 ///
304 /// let reader = Reader::open_readfile(
305 /// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
306 ///
307 /// let subnet = IpNetwork::V4("192.168.0.0/16".parse().unwrap());
308 /// for result in reader.within(subnet, Default::default()).unwrap() {
309 /// match result {
310 /// Ok(lookup) => {
311 /// let network = lookup.network().unwrap();
312 /// println!("Found: {}", network);
313 /// }
314 /// Err(e) => eprintln!("Error: {}", e),
315 /// }
316 /// }
317 /// ```
318 ///
319 /// Include networks without data:
320 /// ```
321 /// use ipnetwork::IpNetwork;
322 /// use maxminddb::{Reader, WithinOptions};
323 ///
324 /// let reader = Reader::open_readfile(
325 /// "test-data/test-data/MaxMind-DB-test-mixed-24.mmdb").unwrap();
326 ///
327 /// let opts = WithinOptions::default().include_networks_without_data();
328 /// for result in reader.within("1.0.0.0/8".parse().unwrap(), opts).unwrap() {
329 /// let lookup = result.unwrap();
330 /// if !lookup.has_data() {
331 /// println!("Network {} has no data", lookup.network().unwrap());
332 /// }
333 /// }
334 /// ```
335 pub fn within(
336 &'de self,
337 cidr: IpNetwork,
338 options: WithinOptions,
339 ) -> Result<Within<'de, S>, MaxMindDbError> {
340 let ip_address = cidr.network();
341 let prefix_len = cidr.prefix() as usize;
342 let ip_int = IpInt::new(ip_address);
343 let bit_count = ip_int.bit_count();
344
345 let mut node = self.start_node(bit_count);
346 let node_count = self.metadata.node_count as usize;
347
348 let mut stack: Vec<WithinNode> = Vec::with_capacity(bit_count - prefix_len);
349
350 // Traverse down the tree to the level that matches the cidr mark
351 let mut depth = 0_usize;
352 for i in 0..prefix_len {
353 let bit = ip_int.get_bit(i);
354 node = self.read_node(node, bit as usize)?;
355 depth = i + 1; // We've now traversed i+1 bits (bits 0 through i)
356
357 if node >= node_count {
358 // We've hit a data node or dead end before we exhausted our prefix.
359 // This means the requested CIDR is contained in a single record.
360 break;
361 }
362 }
363
364 // Always push the node - it could be:
365 // - A data node (> node_count): will be yielded as a single record
366 // - The empty node (== node_count): will be skipped unless include_networks_without_data
367 // - An internal node (< node_count): will be traversed to find all contained records
368 stack.push(WithinNode {
369 node,
370 ip_int,
371 prefix_len: depth,
372 });
373
374 let within = Within {
375 reader: self,
376 node_count,
377 stack,
378 options,
379 };
380
381 Ok(within)
382 }
383
384 fn find_address_in_tree(&self, ip_int: &IpInt) -> Result<(usize, usize), MaxMindDbError> {
385 let bit_count = ip_int.bit_count();
386 let mut node = self.start_node(bit_count);
387
388 let node_count = self.metadata.node_count as usize;
389 let mut prefix_len = bit_count;
390
391 for i in 0..bit_count {
392 if node >= node_count {
393 prefix_len = i;
394 break;
395 }
396 let bit = ip_int.get_bit(i);
397 node = self.read_node(node, bit as usize)?;
398 }
399 match node_count {
400 // If node == node_count, it means we hit the placeholder "empty" node
401 // return 0 as the pointer value to signify "not found".
402 _ if node == node_count => Ok((0, prefix_len)),
403 _ if node > node_count => Ok((node, prefix_len)),
404 _ => Err(MaxMindDbError::invalid_database(
405 "invalid node in search tree",
406 )),
407 }
408 }
409
410 #[inline]
411 fn start_node(&self, length: usize) -> usize {
412 if length == 128 {
413 0
414 } else {
415 self.ipv4_start
416 }
417 }
418
419 /// Find the IPv4 start node and the bit depth at which it was found.
420 /// Returns (node, depth) where depth is how far into the tree we traversed.
421 fn find_ipv4_start(&self) -> Result<(usize, usize), MaxMindDbError> {
422 if self.metadata.ip_version != 6 {
423 return Ok((0, 0));
424 }
425
426 // We are looking up an IPv4 address in an IPv6 tree. Skip over the
427 // first 96 nodes.
428 let mut node: usize = 0;
429 let mut depth: usize = 0;
430 for i in 0_u8..96 {
431 if node >= self.metadata.node_count as usize {
432 depth = i as usize;
433 break;
434 }
435 node = self.read_node(node, 0)?;
436 depth = (i + 1) as usize;
437 }
438 Ok((node, depth))
439 }
440
441 #[inline(always)]
442 pub(crate) fn read_node(
443 &self,
444 node_number: usize,
445 index: usize,
446 ) -> Result<usize, MaxMindDbError> {
447 let buf = self.buf.as_ref();
448 let base_offset = node_number * (self.metadata.record_size as usize) / 4;
449
450 let val = match self.metadata.record_size {
451 24 => {
452 let offset = base_offset + index * 3;
453 (buf[offset] as usize) << 16
454 | (buf[offset + 1] as usize) << 8
455 | buf[offset + 2] as usize
456 }
457 28 => {
458 let middle = if index != 0 {
459 buf[base_offset + 3] & 0x0F
460 } else {
461 (buf[base_offset + 3] & 0xF0) >> 4
462 };
463 let offset = base_offset + index * 4;
464 (middle as usize) << 24
465 | (buf[offset] as usize) << 16
466 | (buf[offset + 1] as usize) << 8
467 | buf[offset + 2] as usize
468 }
469 32 => {
470 let offset = base_offset + index * 4;
471 (buf[offset] as usize) << 24
472 | (buf[offset + 1] as usize) << 16
473 | (buf[offset + 2] as usize) << 8
474 | buf[offset + 3] as usize
475 }
476 s => {
477 return Err(MaxMindDbError::invalid_database(format!(
478 "unknown record size: {s}"
479 )))
480 }
481 };
482 Ok(val)
483 }
484
485 /// Resolves a pointer from the search tree to an offset in the data section.
486 #[inline]
487 pub(crate) fn resolve_data_pointer(&self, pointer: usize) -> Result<usize, MaxMindDbError> {
488 let resolved = pointer - (self.metadata.node_count as usize) - 16;
489
490 // Check bounds using pointer_base which marks the start of the data section
491 if resolved >= (self.buf.as_ref().len() - self.pointer_base) {
492 return Err(MaxMindDbError::invalid_database(
493 "the MaxMind DB file's data pointer resolves to an invalid location",
494 ));
495 }
496
497 Ok(resolved)
498 }
499
500 /// Performs comprehensive validation of the MaxMind DB file.
501 ///
502 /// This method validates:
503 /// - Metadata section: format versions, required fields, and value constraints
504 /// - Search tree: traverses all networks to verify tree structure integrity
505 /// - Data section separator: validates the 16-byte separator between tree and data
506 /// - Data section: verifies all data records referenced by the search tree
507 ///
508 /// The verifier is stricter than the MaxMind DB specification and may return
509 /// errors on some databases that are still readable by normal operations.
510 /// This method is useful for:
511 /// - Validating database files after download or generation
512 /// - Debugging database corruption issues
513 /// - Ensuring database integrity in critical applications
514 ///
515 /// Note: Verification traverses the entire database and may be slow on large files.
516 /// The method is thread-safe and can be called on an active Reader.
517 ///
518 /// # Example
519 ///
520 /// ```
521 /// use maxminddb::Reader;
522 ///
523 /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
524 /// reader.verify().expect("Database should be valid");
525 /// ```
526 pub fn verify(&self) -> Result<(), MaxMindDbError> {
527 self.verify_metadata()?;
528 self.verify_database()
529 }
530
531 fn verify_metadata(&self) -> Result<(), MaxMindDbError> {
532 let m = &self.metadata;
533
534 if m.binary_format_major_version != 2 {
535 return Err(MaxMindDbError::invalid_database(format!(
536 "binary_format_major_version - Expected: 2 Actual: {}",
537 m.binary_format_major_version
538 )));
539 }
540 if m.binary_format_minor_version != 0 {
541 return Err(MaxMindDbError::invalid_database(format!(
542 "binary_format_minor_version - Expected: 0 Actual: {}",
543 m.binary_format_minor_version
544 )));
545 }
546 if m.database_type.is_empty() {
547 return Err(MaxMindDbError::invalid_database(
548 "database_type - Expected: non-empty string Actual: \"\"",
549 ));
550 }
551 if m.description.is_empty() {
552 return Err(MaxMindDbError::invalid_database(
553 "description - Expected: non-empty map Actual: {}",
554 ));
555 }
556 if m.ip_version != 4 && m.ip_version != 6 {
557 return Err(MaxMindDbError::invalid_database(format!(
558 "ip_version - Expected: 4 or 6 Actual: {}",
559 m.ip_version
560 )));
561 }
562 if m.record_size != 24 && m.record_size != 28 && m.record_size != 32 {
563 return Err(MaxMindDbError::invalid_database(format!(
564 "record_size - Expected: 24, 28, or 32 Actual: {}",
565 m.record_size
566 )));
567 }
568 if m.node_count == 0 {
569 return Err(MaxMindDbError::invalid_database(
570 "node_count - Expected: positive integer Actual: 0",
571 ));
572 }
573 Ok(())
574 }
575
576 fn verify_database(&self) -> Result<(), MaxMindDbError> {
577 let offsets = self.verify_search_tree()?;
578 self.verify_data_section_separator()?;
579 self.verify_data_section(offsets)
580 }
581
582 fn verify_search_tree(&self) -> Result<HashSet<usize>, MaxMindDbError> {
583 let mut offsets = HashSet::new();
584 let opts = WithinOptions::default().include_networks_without_data();
585
586 // Maximum number of networks we can expect in a valid database.
587 // A database with N nodes can have at most 2N data entries (each leaf node
588 // can have data). We add some margin for safety.
589 let max_iterations = (self.metadata.node_count as usize).saturating_mul(3);
590 let mut iteration_count = 0usize;
591
592 for result in self.networks(opts)? {
593 let lookup = result?;
594 if let Some(offset) = lookup.offset() {
595 offsets.insert(offset);
596 }
597
598 iteration_count += 1;
599 if iteration_count > max_iterations {
600 return Err(MaxMindDbError::invalid_database(format!(
601 "search tree appears to have a cycle or invalid structure (exceeded {max_iterations} iterations)"
602 )));
603 }
604 }
605 Ok(offsets)
606 }
607
608 fn verify_data_section_separator(&self) -> Result<(), MaxMindDbError> {
609 let separator_start =
610 self.metadata.node_count as usize * self.metadata.record_size as usize / 4;
611 let separator_end = separator_start + DATA_SECTION_SEPARATOR_SIZE;
612
613 if separator_end > self.buf.as_ref().len() {
614 return Err(MaxMindDbError::invalid_database_at(
615 "data section separator extends past end of file",
616 separator_start,
617 ));
618 }
619
620 let separator = &self.buf.as_ref()[separator_start..separator_end];
621
622 for &b in separator {
623 if b != 0 {
624 return Err(MaxMindDbError::invalid_database_at(
625 format!("unexpected byte in data separator: {separator:?}"),
626 separator_start,
627 ));
628 }
629 }
630 Ok(())
631 }
632
633 fn verify_data_section(&self, offsets: HashSet<usize>) -> Result<(), MaxMindDbError> {
634 let data_section = &self.buf.as_ref()[self.pointer_base..];
635
636 // Verify each offset from the search tree points to valid, decodable data
637 for &offset in &offsets {
638 if offset >= data_section.len() {
639 return Err(MaxMindDbError::invalid_database_at(
640 format!(
641 "search tree pointer is beyond data section (len: {})",
642 data_section.len()
643 ),
644 offset,
645 ));
646 }
647
648 let mut dec = decoder::Decoder::new(data_section, offset);
649
650 // Try to skip/decode the value to verify it's valid
651 if let Err(e) = dec.skip_value_for_verification() {
652 return Err(MaxMindDbError::invalid_database_at(
653 format!("decoding error: {e}"),
654 offset,
655 ));
656 }
657 }
658
659 Ok(())
660 }
661}
662
663fn find_metadata_start(buf: &[u8]) -> Result<usize, MaxMindDbError> {
664 const METADATA_START_MARKER: &[u8] = b"\xab\xcd\xefMaxMind.com";
665
666 memchr::memmem::rfind(buf, METADATA_START_MARKER)
667 .map(|x| x + METADATA_START_MARKER.len())
668 .ok_or_else(|| {
669 MaxMindDbError::invalid_database("could not find MaxMind DB metadata in file")
670 })
671}