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    {
77        let _ = temp_dir.close();
78    }
79}
80
81/// Runs the given async closure with a temporary directory.
82pub async fn run_with_tempdir<F, Fut>(prefix: &str, f: F)
83where
84    F: FnOnce(PathBuf) -> Fut,
85    Fut: Future<Output = ()>,
86{
87    let temp_dir = TempDir::with_prefix(prefix).unwrap();
88    let temp_dir_path = temp_dir.path().to_path_buf();
89    f(temp_dir_path).await;
90    #[cfg(not(windows))]
91    {
92        let _ = temp_dir.close();
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use std::net::SocketAddr;
100
101    #[test]
102    fn test_extract_value_with_equals() {
103        let line = "key=value some other text";
104        assert_eq!(extract_value("key", line), Some("value"));
105    }
106
107    #[test]
108    fn test_extract_value_with_colon() {
109        let line = "key: value, more text here";
110        assert_eq!(extract_value("key", line), Some("value"));
111    }
112
113    #[test]
114    fn test_extract_value_not_found() {
115        let line = "unrelated text";
116        assert_eq!(extract_value("key", line), None);
117    }
118
119    #[test]
120    fn test_extract_value_equals_no_space() {
121        let line = "INFO key=";
122        assert_eq!(extract_value("key", line), Some(""))
123    }
124
125    #[test]
126    fn test_extract_value_colon_no_comma() {
127        let line = "INFO key: value";
128        assert_eq!(extract_value("key", line), Some("value"))
129    }
130
131    #[test]
132    fn test_extract_http_address() {
133        let line = "INFO [07-01|13:20:42.774] HTTP server started                      endpoint=127.0.0.1:8545 auth=false prefix= cors= vhosts=localhost";
134        assert_eq!(
135            extract_endpoint("endpoint=", line),
136            Some(SocketAddr::from(([127, 0, 0, 1], 8545)))
137        );
138    }
139
140    #[test]
141    fn test_extract_udp_address() {
142        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\")], .. }";
143        assert_eq!(
144            extract_endpoint("IpV4 TCP Socket: ", line),
145            Some(SocketAddr::from(([0, 0, 0, 0], 30303)))
146        );
147    }
148
149    #[test]
150    fn test_unused_port() {
151        let port = unused_port();
152        assert!(port > 0);
153    }
154
155    #[test]
156    fn test_run_with_tempdir_sync() {
157        run_with_tempdir_sync("test_prefix", |path| {
158            assert!(path.exists(), "Temporary directory should exist");
159            assert!(path.is_dir(), "Temporary directory should be a directory");
160        });
161    }
162
163    #[tokio::test]
164    async fn test_run_with_tempdir_async() {
165        run_with_tempdir("test_prefix", |path| async move {
166            assert!(path.exists(), "Temporary directory should exist");
167            assert!(path.is_dir(), "Temporary directory should be a directory");
168        })
169        .await;
170    }
171}