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),