use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::net::Ipv4Addr;
use anyhow::Context as _;
use anyhow::Result;
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct TcpEntry {
pub addr: Ipv4Addr,
pub port: u16,
pub inode: u64,
}
fn parse_tcp_line(line: &str) -> Result<TcpEntry> {
let mut parts = line.split_whitespace().skip(1);
let local_addr_str = parts
.next()
.context("failed to find 'local address' component")?;
let (addr_str, port_str) = local_addr_str
.split_once(':')
.with_context(|| format!("encountered malformed local address in proc tcp line: {line}"))?;
let addr = u32::from_str_radix(addr_str, 16)
.with_context(|| format!("encountered malformed address in proc tcp line: {line}"))?
.to_be();
let port = u16::from_str_radix(port_str, 16)
.with_context(|| format!("encountered malformed port number in proc tcp line: {line}"))?;
let mut parts = parts.skip(7);
let inode_str = parts.next().context("failed to find 'inode' component")?;
let inode = inode_str
.parse::<u64>()
.with_context(|| format!("encountered malformed inode in proc tcp line: {line}"))?;
let entry = TcpEntry {
addr: Ipv4Addr::from(addr),
port,
inode,
};
Ok(entry)
}
#[derive(Debug)]
struct TcpEntryIter<R> {
reader: R,
line: String,
skipped_header: bool,
}
impl<R> Iterator for TcpEntryIter<R>
where
R: BufRead,
{
type Item = Result<TcpEntry>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let () = self.line.clear();
match self.reader.read_line(&mut self.line) {
Err(err) => return Some(Err(err.into())),
Ok(0) => break None,
Ok(_) => {
let line_str = self.line.trim();
if !line_str.is_empty() {
if !self.skipped_header {
self.skipped_header = true;
} else {
let result = parse_tcp_line(line_str);
break Some(result)
}
}
},
}
}
}
}
fn parse_file<R>(reader: R) -> impl Iterator<Item = Result<TcpEntry>>
where
R: Read,
{
TcpEntryIter {
reader: BufReader::with_capacity(16 * 1024, reader),
line: String::new(),
skipped_header: false,
}
}
pub(crate) fn parse(pid: u32) -> Result<impl Iterator<Item = Result<TcpEntry>>> {
let path = format!("/proc/{pid}/net/tcp");
let file = File::open(&path).with_context(|| format!("failed to open proc tcp file `{path}`"))?;
let iter = parse_file(file);
Ok(iter)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tcp_line_parsing() {
let lines = r#"
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:B1AB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 1109147 1 00000000481c5bfd 100 0 0 10 0
1: 00000000:D431 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 5883 1 000000009861ba23 100 0 0 10 0
2: 0C00A8C0:D29C 8B1715B2:03E1 01 00000000:00000000 02:00000F09 00000000 1000 0 852603 2 00000000f91bdecb 35 4 14 4 4
3: 0C00A8C0:D44A 8B1715B2:03E1 01 00000000:00000000 02:00000F0A 00000000 1000 0 847558 2 00000000907a55a3 35 4 14 10 -1
"#;
let entries = parse_file(lines.as_bytes());
let () = entries.for_each(|entry| {
let _entry = entry.unwrap();
});
let mut entries = parse_file(lines.as_bytes());
let expected = TcpEntry {
addr: Ipv4Addr::new(127, 0, 0, 1),
port: 0xB1AB,
inode: 1109147,
};
assert_eq!(entries.next().unwrap().unwrap(), expected);
}
}