diff --git a/lading/src/bin/payloadtool.rs b/lading/src/bin/payloadtool.rs index fc6f70bb5..e3227c625 100644 --- a/lading/src/bin/payloadtool.rs +++ b/lading/src/bin/payloadtool.rs @@ -368,10 +368,6 @@ fn generate_and_check( } #[expect(clippy::too_many_lines)] -#[expect( - clippy::expect_used, - reason = "FIXME: maximum_prebuild_cache_size_bytes is user-supplied config; a zero value should surface as a recoverable error rather than panicking. Tracked for follow-up." -)] fn check_generator(config: &generator::Config, args: &Args) -> Result> { match &config.inner { generator::Inner::FileGen(g) => { @@ -398,26 +394,26 @@ fn check_generator(config: &generator::Config, args: &Args) -> Result { let max_block_size = UDP_PACKET_LIMIT_BYTES; #[expect(clippy::cast_possible_truncation)] let total_bytes = NonZeroU32::new(g.maximum_prebuild_cache_size_bytes.as_u128() as u32) - .expect("Non-zero max prebuild cache size"); + .ok_or_else(|| anyhow!("maximum_prebuild_cache_size_bytes must be non-zero"))?; generate_and_check(&g.variant, g.seed, total_bytes, max_block_size, args) } generator::Inner::Tcp(g) => { #[expect(clippy::cast_possible_truncation)] let total_bytes = NonZeroU32::new(g.maximum_prebuild_cache_size_bytes.as_u128() as u32) - .expect("Non-zero max prebuild cache size"); + .ok_or_else(|| anyhow!("maximum_prebuild_cache_size_bytes must be non-zero"))?; generate_and_check(&g.variant, g.seed, total_bytes, g.maximum_block_size, args) } generator::Inner::Udp(g) => { #[expect(clippy::cast_possible_truncation)] let total_bytes = NonZeroU32::new(g.maximum_prebuild_cache_size_bytes.as_u128() as u32) - .expect("Non-zero max prebuild cache size"); + .ok_or_else(|| anyhow!("maximum_prebuild_cache_size_bytes must be non-zero"))?; let max_block_size = UDP_PACKET_LIMIT_BYTES; generate_and_check(&g.variant, g.seed, total_bytes, max_block_size, args) } @@ -431,7 +427,7 @@ fn check_generator(config: &generator::Config, args: &Args) -> Result { @@ -451,19 +447,19 @@ fn check_generator(config: &generator::Config, args: &Args) -> Result { #[expect(clippy::cast_possible_truncation)] let total_bytes = NonZeroU32::new(g.maximum_prebuild_cache_size_bytes.as_u128() as u32) - .expect("Non-zero max prebuild cache size"); + .ok_or_else(|| anyhow!("maximum_prebuild_cache_size_bytes must be non-zero"))?; generate_and_check(&g.variant, g.seed, total_bytes, g.maximum_block_size, args) } generator::Inner::UnixStream(g) => { #[expect(clippy::cast_possible_truncation)] let total_bytes = NonZeroU32::new(g.maximum_prebuild_cache_size_bytes.as_u128() as u32) - .expect("Non-zero max prebuild cache size"); + .ok_or_else(|| anyhow!("maximum_prebuild_cache_size_bytes must be non-zero"))?; generate_and_check(&g.variant, g.seed, total_bytes, g.maximum_block_size, args) } generator::Inner::PassthruFile(g) => { #[expect(clippy::cast_possible_truncation)] let total_bytes = NonZeroU32::new(g.maximum_prebuild_cache_size_bytes.as_u128() as u32) - .expect("Non-zero max prebuild cache size"); + .ok_or_else(|| anyhow!("maximum_prebuild_cache_size_bytes must be non-zero"))?; generate_and_check(&g.variant, g.seed, total_bytes, g.maximum_block_size, args) } generator::Inner::ProcessTree(_) => { diff --git a/lading/src/generator/grpc.rs b/lading/src/generator/grpc.rs index a504c6cc5..b3cc6fb20 100644 --- a/lading/src/generator/grpc.rs +++ b/lading/src/generator/grpc.rs @@ -83,6 +83,15 @@ pub enum Error { #[source] source: Box, }, + /// `config.target_uri` is not a valid URI, or is missing the required + /// path-and-query component. + #[error("invalid target_uri {uri}: {reason}")] + InvalidTargetUri { + /// User-supplied URI string. + uri: String, + /// Cause of rejection (URI parse error or missing path). + reason: String, + }, } impl From for Error { @@ -185,17 +194,16 @@ impl Grpc { /// /// # Errors /// - /// Creation will fail if the underlying governor capacity exceeds u32. + /// Creation will fail if the underlying governor capacity exceeds u32, or + /// returns [`Error::InvalidTargetUri`] when `config.target_uri` fails to + /// parse or has no path-and-query component (e.g. `http://host` with no + /// `/service/Method` suffix). /// /// # Panics /// /// Function will panic if user has passed zero values for any byte /// values. Sharp corners. #[expect(clippy::cast_possible_truncation)] - #[expect( - clippy::expect_used, - reason = "FIXME: config.target_uri is user-supplied; parsing and the required path_and_query check should surface as Error variants instead of panicking. Tracked for follow-up." - )] pub fn new( general: General, config: Config, @@ -227,12 +235,20 @@ impl Grpc { )?, }; - let target_uri = - uri::Uri::try_from(config.target_uri.clone()).expect("target_uri must be valid"); - let rpc_path = target_uri - .path_and_query() - .cloned() - .expect("target_uri should have an RPC path"); + let target_uri = uri::Uri::try_from(config.target_uri.clone()).map_err(|source| { + Error::InvalidTargetUri { + uri: config.target_uri.clone(), + reason: source.to_string(), + } + })?; + let rpc_path = + target_uri + .path_and_query() + .cloned() + .ok_or_else(|| Error::InvalidTargetUri { + uri: config.target_uri.clone(), + reason: "missing path-and-query component".to_string(), + })?; Ok(Self { target_uri, rpc_path, diff --git a/lading/src/generator/tcp.rs b/lading/src/generator/tcp.rs index e3e2a3146..8323d7d6c 100644 --- a/lading/src/generator/tcp.rs +++ b/lading/src/generator/tcp.rs @@ -108,6 +108,18 @@ pub enum Error { #[source] source: Box, }, + /// Failed to resolve `config.addr` to a socket address. + #[error("Failed to resolve TCP address {addr}: {source}")] + AddrParse { + /// User-supplied address string. + addr: String, + /// Underlying resolver error. + #[source] + source: Box, + }, + /// `config.addr` resolved to an empty set of socket addresses. + #[error("TCP address {0} resolved to no socket addresses")] + EmptyAddrResolution(String), } #[derive(Debug)] @@ -124,17 +136,9 @@ impl Tcp { /// /// # Errors /// - /// Creation will fail if the underlying governor capacity exceeds u32. - /// - /// # Panics - /// - /// Function will panic if user has passed zero values for any byte - /// values. Sharp corners. + /// Creation will fail if the underlying governor capacity exceeds u32, or + /// if `config.addr` cannot be resolved to a socket address. #[expect(clippy::cast_possible_truncation)] - #[expect( - clippy::expect_used, - reason = "FIXME: config.addr is user-supplied; socket address parsing failures should surface as Error variants instead of panicking. Tracked for follow-up." - )] pub fn new( general: General, config: &Config, @@ -158,9 +162,12 @@ impl Tcp { let addr = config .addr .to_socket_addrs() - .expect("could not convert to socket") + .map_err(|source| Error::AddrParse { + addr: config.addr.clone(), + source: Box::new(source), + })? .next() - .expect("could not convert to socket addr"); + .ok_or_else(|| Error::EmptyAddrResolution(config.addr.clone()))?; let concurrency = ConcurrencyStrategy::new( NonZeroU16::new(config.parallel_connections), diff --git a/lading/src/generator/tcp_rr.rs b/lading/src/generator/tcp_rr.rs index e6ed6a883..8c46378b5 100644 --- a/lading/src/generator/tcp_rr.rs +++ b/lading/src/generator/tcp_rr.rs @@ -92,6 +92,9 @@ pub enum Error { /// Invalid configuration. #[error("invalid config: {0}")] Config(String), + /// `config.addr` is not a valid IP address. + #[error("invalid addr: {0}")] + InvalidAddr(#[from] std::net::AddrParseError), } #[derive(Debug)] @@ -123,15 +126,8 @@ impl TcpRr { /// /// # Errors /// - /// Returns an error if a worker thread panics. - /// - /// # Panics - /// - /// Panics if `addr` cannot be resolved to a socket address. - #[expect( - clippy::expect_used, - reason = "FIXME: config.addr is user-supplied; parse failure should surface as an Error variant instead of panicking. Tracked for follow-up." - )] + /// Returns an error if `config.addr` is not a valid IP address or if a + /// worker thread panics. pub async fn spin(self) -> Result<(), Error> { if self.config.threads > self.config.flows { return Err(Error::Config(format!( @@ -140,7 +136,7 @@ impl TcpRr { ))); } - let ip: IpAddr = self.config.addr.parse().expect("invalid addr"); + let ip: IpAddr = self.config.addr.parse()?; let data_addr = SocketAddr::new(ip, self.config.data_port); let control_addr = SocketAddr::new(ip, self.config.control_port); diff --git a/lading/src/generator/udp.rs b/lading/src/generator/udp.rs index f4ebf2c0a..bccd5136d 100644 --- a/lading/src/generator/udp.rs +++ b/lading/src/generator/udp.rs @@ -119,6 +119,18 @@ pub enum Error { #[source] source: Box, }, + /// Failed to resolve `config.addr` to a socket address. + #[error("Failed to resolve UDP address {addr}: {source}")] + AddrParse { + /// User-supplied address string. + addr: String, + /// Underlying resolver error. + #[source] + source: Box, + }, + /// `config.addr` resolved to an empty set of socket addresses. + #[error("UDP address {0} resolved to no socket addresses")] + EmptyAddrResolution(String), } #[derive(Debug)] @@ -135,17 +147,9 @@ impl Udp { /// /// # Errors /// - /// Creation will fail if the underlying governor capacity exceeds u32. - /// - /// # Panics - /// - /// Function will panic if user has passed zero values for any byte - /// values. Sharp corners. + /// Creation will fail if the underlying governor capacity exceeds u32, or + /// if `config.addr` cannot be resolved to a socket address. #[expect(clippy::cast_possible_truncation)] - #[expect( - clippy::expect_used, - reason = "FIXME: config.addr is user-supplied; socket address parsing failures should surface as Error variants instead of panicking. Tracked for follow-up." - )] pub fn new( general: General, config: &Config, @@ -169,9 +173,12 @@ impl Udp { let addr = config .addr .to_socket_addrs() - .expect("could not convert to socket") + .map_err(|source| Error::AddrParse { + addr: config.addr.clone(), + source: Box::new(source), + })? .next() - .expect("could not convert to socket addr"); + .ok_or_else(|| Error::EmptyAddrResolution(config.addr.clone()))?; let concurrency = ConcurrencyStrategy::new( NonZeroU16::new(config.parallel_connections),