diff --git a/src/routing_table.rs b/src/routing_table.rs index 7802b29..88c1fd0 100644 --- a/src/routing_table.rs +++ b/src/routing_table.rs @@ -21,14 +21,10 @@ pub enum Error { NetstatFail(ExitStatus), #[error("netstat output not non-UTF-8")] NetstatUtf8(FromUtf8Error), - #[error("Unrecognized section name {0:?}")] - NetstatParseUnrecognizedSectionName(String), - #[error("{0:?} section missing headers")] + #[error("no headers follow {0:?} section marker")] NetstatParseNoHeaders(String), #[error("parsing route entry: {0}")] RouteEntryParse(#[from] crate::route_entry::Error), - #[error("CIDR first address neither V4 nor V6")] - CidrNotV4V6, #[error("route entry found before protocol (Internet/Internet6) found.")] EntryBeforeProto, } @@ -67,9 +63,7 @@ impl RoutingTable { proto = match section { "Internet:" => Some(Protocol::V4), "Internet6:" => Some(Protocol::V6), - _ => { - return Err(Error::NetstatParseUnrecognizedSectionName(section.into())) - } + _ => unreachable!(), }; // Next line will contain the column headers if let Some(line) = lines.next() { @@ -88,7 +82,9 @@ impl RoutingTable { if cidr.is_host_address() { let route = route.clone(); let gws = if_router.entry(route.net_if).or_insert_with(Vec::new); - gws.push(cidr.first_address().ok_or(Error::CidrNotV4V6)?); + // The route parser doesn't produce `Any` CIDRs, + // so there's always a first address. + gws.push(cidr.first_address().unwrap_or_else(|| unreachable!())); } } routes.push(route); @@ -123,6 +119,10 @@ impl RoutingTable { } /// Execute `netstat -rn` and return the output +/// +/// # Errors +/// +/// Returns an error if command execution fails, or the output is not UTF-8 pub async fn execute_netstat() -> Result { let output = Command::new(NETSTAT_PATH) .arg("-rn") @@ -135,3 +135,91 @@ pub async fn execute_netstat() -> Result { } String::from_utf8(output.stdout).map_err(Error::NetstatUtf8) } + +#[cfg(test)] +mod tests { + use super::Error; + use crate::{Destination, Entity, RoutingTable}; + use std::{process::ExitStatus, string::FromUtf8Error}; + + include!(concat!(env!("OUT_DIR"), "/sample_table.rs")); + + #[tokio::test] + async fn coverage() { + let rt = RoutingTable::from_netstat_output(SAMPLE_TABLE).expect("parse routing table"); + let _ = format!("{rt:?}"); + let _ = format!( + "{:?}", + Error::NetstatExec(std::io::Error::from_raw_os_error(1)) + ); + let _ = format!("{:?}", Error::NetstatFail(ExitStatus::default())); + // This error is reachable only if the netstat command outputs invalid + // UTF-8. + let from_utf8err = String::from_utf8([0xa0, 0xa1].to_vec()).unwrap_err(); + let _ = format!("{:?}", Error::NetstatUtf8(from_utf8err)); + } + + #[tokio::test] + #[cfg(target_os = "macos")] + async fn live_test() { + let _routing_table = RoutingTable::load_from_netstat() + .await + .expect("parse live routing table"); + } + + #[test] + fn good_table() { + let rt = RoutingTable::from_netstat_output(SAMPLE_TABLE).expect("parse routing table"); + let entry = rt.find_route_entry("1.1.1.1".parse().unwrap()); + dbg!(&entry); + assert!(entry.is_some()); + let entry = entry.unwrap(); + assert!(matches!( + entry.dest, + Destination { + entity: Entity::Default, + zone: None + } + )); + // Coverage of debug formatting + let _ = format!("{rt:?}"); + } + + #[test] + fn missing_headers() { + for section in ["", "6"] { + let input = format!("{SAMPLE_TABLE}Internet{section}:\n"); + let result = RoutingTable::from_netstat_output(&input); + assert!(matches!(result, Err(Error::NetstatParseNoHeaders(_)))); + // Coverage of debug formatting + let _ = format!("{:?}", result.unwrap_err()); + } + } + + #[test] + fn stray_entry() { + let input = format!("extra stuff\n{SAMPLE_TABLE}"); + let result = RoutingTable::from_netstat_output(&input); + assert!(matches!(result, Err(Error::EntryBeforeProto))); + // Coverage of debug formatting + let _ = format!("{:?}", result.unwrap_err()); + } + + #[test] + fn bad_entry() { + let input = format!("{SAMPLE_TABLE}How now brown cow.\n"); + let result = RoutingTable::from_netstat_output(&input); + dbg!(&result); + assert!(matches!( + result, + Err(Error::RouteEntryParse( + crate::route_entry::Error::ParseIPv4AddrBadInt { + addr: _, + err: std::num::ParseIntError { .. }, + } + )) + )); + // Coverage of debug formatting + let _ = format!("{:?}", result.unwrap_err()); + } +} diff --git a/tests/live.rs b/tests/live.rs index 422a5db..753bcf3 100644 --- a/tests/live.rs +++ b/tests/live.rs @@ -1,4 +1,4 @@ -#![allow(clippy::missing_panics_doc)] +#![allow(clippy::missing_panics_doc, clippy::missing_errors_doc)] use anyhow::Result; use macos_routing_table::{execute_netstat, RoutingTable};