Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 101 additions & 31 deletions libs/mongodb-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,49 +137,68 @@ impl FromStr for MongoConnectionString {
let hosts: Result<Vec<_>, Error> = hosts_section
.split(',')
.map(|address| {
let mut parts = address.split(':');

let hostname = match parts.next() {
Some(part) => {
if part.is_empty() {
let (hostname, port) = if address.starts_with('[') {
let end_bracket_idx = match address.rfind(']') {
Some(end_bracket_idx) => end_bracket_idx,
None => {
return Err(ErrorKind::invalid_argument(format!(
"invalid server address: \"{address}\"; hostname cannot be empty"
"invalid server address: \"{address}\"; missing closing bracket for IPv6 address"
))
.into());
}
part
}
None => {
return Err(
ErrorKind::invalid_argument(format!("invalid server address: \"{address}\"")).into(),
);
}
};
};

let port = match parts.next() {
Some(part) => {
let port = u16::from_str(part).map_err(|_| {
let (host, port_str) = address.split_at(end_bracket_idx + 1);

let port = if !port_str.is_empty() {
let port_str = port_str.strip_prefix(':').ok_or_else(|| {
ErrorKind::invalid_argument(format!(
"port must be valid 16-bit unsigned integer, instead got: {part}"
"invalid server address: \"{address}\"; invalid characters after IPv6 address"
))
})?;

if port == 0 {
return Err(ErrorKind::invalid_argument(format!(
"invalid server address: \"{address}\"; port must be non-zero"
))
.into());
Some(parse_port(port_str, address)?)
} else {
None
};

(host, port)
Comment on lines +140 to +165
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for empty IPv6 addresses. The code should check that the content between brackets is not empty to prevent addresses like [] or []:27017 from being accepted. Consider adding a check after line 148 to validate that the extracted IPv6 address (without brackets) is not empty:

let ipv6_addr = &host[1..host.len()-1];
if ipv6_addr.is_empty() {
    return Err(ErrorKind::invalid_argument(format!(
        "invalid server address: \"{address}\"; IPv6 address cannot be empty"
    ))
    .into());
}

Copilot uses AI. Check for mistakes.
} else {
let mut parts = address.split(':');
let hostname = match parts.next() {
Some(part) => {
if part.is_empty() {
return Err(ErrorKind::invalid_argument(format!(
"invalid server address: \"{address}\"; hostname cannot be empty"
))
.into());
}
part
}
if parts.next().is_some() {
return Err(ErrorKind::invalid_argument(format!(
"address \"{address}\" contains more than one unescaped ':'"
))
.into());
None => {
return Err(
ErrorKind::invalid_argument(format!("invalid server address: \"{address}\"")).into(),
);
}
};

Some(port)
}
None => None,
let port = match parts.next() {
Some(part) => {
let port = parse_port(part, address)?;

if parts.next().is_some() {
return Err(ErrorKind::invalid_argument(format!(
"address \"{address}\" contains more than one unescaped ':'"
))
.into());
}

Some(port)
}
None => None,
};

(hostname, port)
};

Ok((hostname.to_lowercase(), port))
Expand Down Expand Up @@ -222,6 +241,23 @@ fn percent_decode(s: &str, err_message: &str) -> Result<String, Error> {
}
}

fn parse_port(port_str: &str, address: &str) -> Result<u16, Error> {
let port = u16::from_str(port_str).map_err(|_| {
ErrorKind::invalid_argument(format!(
"port must be valid 16-bit unsigned integer, instead got: {port_str}"
))
})?;

if port == 0 {
return Err(ErrorKind::invalid_argument(format!(
"invalid server address: \"{address}\"; port must be non-zero"
))
.into());
}

Ok(port)
}

#[cfg(test)]
mod tests {
use crate::MongoConnectionString;
Expand Down Expand Up @@ -301,4 +337,38 @@ mod tests {
hosts
);
}

#[test]
fn ipv6_host() {
let s = "mongodb://[::1]/test";
let MongoConnectionString { hosts, .. } = s.parse().unwrap();
assert_eq!(vec![(String::from("[::1]"), None)], hosts);
}

#[test]
fn ipv6_host_and_port() {
let s = "mongodb://[::1]:27017/test";
let MongoConnectionString { hosts, .. } = s.parse().unwrap();
assert_eq!(vec![(String::from("[::1]"), Some(27017))], hosts);
}

#[test]
fn multiple_hosts_including_ipv6() {
let s = "mongodb://[::1]:27017,localhost:27018/test";
let MongoConnectionString { hosts, .. } = s.parse().unwrap();

assert_eq!(
vec![
(String::from("[::1]"), Some(27017)),
(String::from("localhost"), Some(27018))
],
hosts
);
}

#[test]
fn ipv6_host_and_zero_port() {
let s = "mongodb://[::1]:0/test";
assert!(s.parse::<MongoConnectionString>().is_err());
}
}