alloy_node_bindings/
utils.rs

1//! Utility functions for the node bindings.
2
3use std::{
4    borrow::Cow,
5    future::Future,
6    net::{SocketAddr, TcpListener},
7    path::PathBuf,
8};
9use tempfile::TempDir;
10
11/// A bit of hack to find an unused TCP port.
12///
13/// Does not guarantee that the given port is unused after the function exists, just that it was
14/// unused before the function started (i.e., it does not reserve a port).
15pub(crate) fn unused_port() -> u16 {
16    let listener = TcpListener::bind("127.0.0.1:0")
17        .expect("Failed to create TCP listener to find unused port");
18
19    let local_addr =
20        listener.local_addr().expect("Failed to read TCP listener local_addr to find unused port");
21    local_addr.port()
22}
23
24/// Extracts the value for the given key from the line of text.
25///
26/// It supports keys that end with '=' or ': '.
27/// For keys end with '=', find value until ' ' is encountered or end of line
28/// For keys end with ':', find value until ',' is encountered or end of line
29pub(crate) fn extract_value<'a>(key: &str, line: &'a str) -> Option<&'a str> {
30    let mut key_equal = Cow::from(key);
31    let mut key_colon = Cow::from(key);
32
33    // Prepare both key variants
34    if !key_equal.ends_with('=') {
35        key_equal = format!("{key}=").into();
36    }
37    if !key_colon.ends_with(": ") {
38        key_colon = format!("{key}: ").into();
39    }
40
41    // Try to find the key with '='
42    if let Some(pos) = line.find(key_equal.as_ref()) {
43        let start = pos + key_equal.len();
44        let end = line[start..].find(' ').map(|i| start + i).unwrap_or(line.len());
45        if start <= line.len() && end <= line.len() {
46            return Some(line[start..end].trim());
47        }
48    }
49
50    // If not found, try to find the key with ': '
51    if let Some(pos) = line.find(key_colon.as_ref()) {
52        let start = pos + key_colon.len();
53        let end = line[start..].find(',').map(|i| start + i).unwrap_or(line.len()); // Assuming comma or end of line
54        if start <= line.len() && end <= line.len() {
55            return Some(line[start..end].trim());
56        }
57    }
58
59    // If neither variant matches, return None
60    None
61}
62
63/// Extracts the endpoint from the given line.
64pub(crate) fn extract_endpoint(key: &str, line: &str) -> Option<SocketAddr> {
65    extract_value(key, line)
66        .map(|val| val.trim_start_matches("Some(").trim_end_matches(')'))
67        .and_then(|val| val.parse().ok())
68}
69
70/// Runs the given closure with a temporary directory.
71pub fn run_with_tempdir_sync(prefix: &str, f: impl FnOnce(PathBuf)) {
72    let temp_dir = TempDir::with_prefix(prefix).unwrap();
73    let temp_dir_path = temp_dir.path().to_path_buf();
74    f(temp_dir_path);
75    #[cfg(not(windows))]
76    temp_dir.close().unwrap();
77}
78
79/// Runs the given async closure with a temporary directory.
80pub async fn run_with_tempdir<F, Fut>(prefix: &str, f: F)
81where
82    F: FnOnce(PathBuf) -> Fut,
83    Fut: Future<Output = ()>,
84{
85    let temp_dir = TempDir::with_prefix(prefix).unwrap();
86    let temp_dir_path = temp_dir.path().to_path_buf();
87    f(temp_dir_path).await;
88    #[cfg(not(windows))]
89    temp_dir.close().unwrap();
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use std::net::SocketAddr;
96
97    #[test]
98    fn test_extract_value_with_equals() {
99        let line = "key=value some other text";
100        assert_eq!(extract_value("key", line), Some("value"));
101    }
102
103    #[test]
104    fn test_extract_value_with_colon() {
105        let line = "key: value, more text here";
106        assert_eq!(extract_value("key", line), Some("value"));
107    }
108
109    #[test]
110    fn test_extract_value_not_found() {
111        let line = "unrelated text";
112        assert_eq!(extract_value("key", line), None);
113    }
114
115    #[test]
116    fn test_extract_value_equals_no_space() {
117        let line = "INFO key=";
118        assert_eq!(extract_value("key", line), Some(""))
119    }
120
121    #[test]
122    fn test_extract_value_colon_no_comma() {
123        let line = "INFO key: value";
124        assert_eq!(extract_value("key", line), Some("value"))
125    }
126
127    #[test]
128    fn test_extract_http_address() {
129        let line = "INFO [07-01|13:20:42.774] HTTP server started                      endpoint=127.0.0.1:8545 auth=false prefix= cors= vhosts=localhost";
130        assert_eq!(
131            extract_endpoint("endpoint=", line),
132            Some(SocketAddr::from(([127, 0, 0, 1], 8545)))
133        );
134    }
135
136    #[test]
137    fn test_extract_udp_address() {
138        let line = "Updated local ENR enr=Enr { id: Some(\"v4\"), seq: 2, NodeId: 0x04dad428038b4db230fc5298646e137564fc6861662f32bdbf220f31299bdde7, signature: \"416520d69bfd701d95f4b77778970a5c18fa86e4dd4dc0746e80779d986c68605f491c01ef39cd3739fdefc1e3558995ad2f5d325f9e1db795896799e8ee94a3\", IpV4 UDP Socket: Some(0.0.0.0:30303), IpV6 UDP Socket: None, IpV4 TCP Socket: Some(0.0.0.0:30303), IpV6 TCP Socket: None, Other Pairs: [(\"eth\", \"c984fc64ec0483118c30\"), (\"secp256k1\", \"a103aa181e8fd5df651716430f1d4b504b54d353b880256f56aa727beadd1b7a9766\")], .. }";
139        assert_eq!(
140            extract_endpoint("IpV4 TCP Socket: ", line),
141            Some(SocketAddr::from(([0, 0, 0, 0], 30303)))
142        );
143    }
144
145    #[test]
146    fn test_unused_port() {
147        let port = unused_port();
148        assert!(port > 0);
149    }
150
151    #[test]
152    fn test_run_with_tempdir_sync() {
153        run_with_tempdir_sync("test_prefix", |path| {
154            assert!(path.exists(), "Temporary directory should exist");
155            assert!(path.is_dir(), "Temporary directory should be a directory");
156        });
157    }
158
159    #[tokio::test]
160    async fn test_run_with_tempdir_async() {
161        run_with_tempdir("test_prefix", |path| async move {
162            assert!(path.exists(), "Temporary directory should exist");
163            assert!(path.is_dir(), "Temporary directory should be a directory");
164        })
165        .await;
166    }
167}