rmap/loaders/
ip_list.rs

1//! Async IPv6 address list parser
2
3use std::net::Ipv6Addr;
4use std::path::PathBuf;
5use std::str::FromStr;
6use tokio::fs::File as AsyncFile;
7use tokio::io::{
8    AsyncBufReadExt, AsyncWriteExt, BufReader as AsyncBufReader, BufWriter as AsyncBufWriter,
9};
10
11/// Load IPv6 addresses from a file asynchronously, one per line
12///
13/// # Arguments
14/// * `file` - Path to the file containing IPv6 addresses
15///
16/// # Returns
17/// * `Result<Vec<[u8; 16]>, String>` - Vector of IPv6 addresses as byte arrays
18///
19/// # Format
20/// - One IPv6 address per line
21/// - Empty lines and lines starting with '#' are ignored
22/// - Addresses can be in any valid IPv6 format (e.g., 2001:db8::1, ::1, etc.)
23pub async fn load_ipv6_addresses_from_file(file: &PathBuf) -> Result<Vec<[u8; 16]>, String> {
24    let file = AsyncFile::open(file)
25        .await
26        .map_err(|e| format!("Failed to open input file: {}", e))?;
27
28    let reader = AsyncBufReader::new(file);
29    let mut lines = reader.lines();
30    let mut addresses = Vec::new();
31    let mut line_num = 0;
32
33    while let Some(line) = lines
34        .next_line()
35        .await
36        .map_err(|e| format!("Failed to read line {}: {}", line_num + 1, e))?
37    {
38        line_num += 1;
39        let line = line.trim();
40
41        if line.is_empty() || line.starts_with('#') {
42            continue;
43        }
44
45        let ip = Ipv6Addr::from_str(line)
46            .map_err(|e| format!("Failed to parse IPv6 address on line {}: {}", line_num, e))?;
47
48        addresses.push(ip.octets());
49    }
50
51    if addresses.is_empty() {
52        return Err("No valid IPv6 addresses found in input file".to_string());
53    }
54
55    Ok(addresses)
56}
57
58/// Write IPv6 addresses to a file asynchronously, one per line
59///
60/// # Arguments
61/// * `addresses` - Slice of IPv6 addresses as byte arrays
62/// * `file_path` - Path where to write the addresses
63///
64/// # Format
65/// - One IPv6 address per line in standard notation
66/// - No comments or headers are written
67pub async fn write_ipv6_addresses_to_file(
68    addresses: &[[u8; 16]],
69    file_path: &PathBuf,
70) -> Result<(), String> {
71    let file = AsyncFile::create(file_path)
72        .await
73        .map_err(|e| format!("Failed to create output file: {}", e))?;
74
75    let mut writer = AsyncBufWriter::new(file);
76
77    for address in addresses {
78        let ipv6 = Ipv6Addr::from(*address);
79        writer
80            .write_all(format!("{}\n", ipv6).as_bytes())
81            .await
82            .map_err(|e| format!("Failed to write address: {}", e))?;
83    }
84
85    writer
86        .flush()
87        .await
88        .map_err(|e| format!("Failed to flush output: {}", e))?;
89
90    Ok(())
91}
92
93#[cfg(test)]
94mod tests {
95    use std::io::Write;
96
97    use super::*;
98    use tempfile::NamedTempFile;
99    use tokio::io::AsyncWriteExt;
100
101    #[tokio::test]
102    async fn test_load_ipv6_addresses() {
103        let mut temp_file = NamedTempFile::new().unwrap();
104        let content = "2001:db8::1\n::1\n# This is a comment\n\nfe80::1\n";
105        temp_file.write_all(content.as_bytes()).unwrap();
106        temp_file.flush().unwrap();
107
108        let path = temp_file.path().to_path_buf();
109        let addresses = load_ipv6_addresses_from_file(&path).await.unwrap();
110
111        assert_eq!(addresses.len(), 3);
112
113        // Verify the addresses are correct
114        let expected = vec![
115            Ipv6Addr::from_str("2001:db8::1").unwrap().octets(),
116            Ipv6Addr::from_str("::1").unwrap().octets(),
117            Ipv6Addr::from_str("fe80::1").unwrap().octets(),
118        ];
119
120        assert_eq!(addresses, expected);
121    }
122
123    #[tokio::test]
124    async fn test_write_ipv6_addresses() {
125        let addresses = vec![
126            Ipv6Addr::from_str("2001:db8::1").unwrap().octets(),
127            Ipv6Addr::from_str("::1").unwrap().octets(),
128        ];
129
130        let temp_file = NamedTempFile::new().unwrap();
131        let path = temp_file.path().to_path_buf();
132
133        write_ipv6_addresses_to_file(&addresses, &path)
134            .await
135            .unwrap();
136
137        // Read back and verify
138        let content = std::fs::read_to_string(&path).unwrap();
139        let lines: Vec<&str> = content.trim().split('\n').collect();
140
141        assert_eq!(lines.len(), 2);
142        assert_eq!(lines[0], "2001:db8::1");
143        assert_eq!(lines[1], "::1");
144    }
145}