use std::collections::HashMap;
#[derive(Debug)]
pub(crate) struct AdoNetString {
pairs: HashMap<String, String>,
}
impl AdoNetString {
pub(crate) fn pairs(&self) -> &HashMap<String, String> {
&self.pairs
}
}
pub(crate) fn parse(input: &str) -> Result<AdoNetString, &'static str> {
if !input.is_ascii() {
return Err("the connection string contains non-ASCII characters");
}
let b = input.as_bytes();
let n = b.len();
let mut i = 0;
let mut pairs = HashMap::new();
while i < n {
while i < n && (b[i] == b';' || b[i].is_ascii_whitespace()) {
i += 1;
}
if i >= n {
break;
}
let mut key = String::new();
loop {
if i >= n || b[i] == b';' {
return Err("key-value pairs must be joined by a `=`");
}
if b[i] == b'=' {
if i + 1 < n && b[i + 1] == b'=' {
key.push('=');
i += 2;
continue;
}
i += 1; break;
}
key.push(b[i] as char);
i += 1;
}
let key = key
.trim_matches(|c: char| c.is_ascii_whitespace())
.to_ascii_lowercase();
if key.is_empty() {
return Err("a key in the connection string is empty");
}
while i < n && b[i].is_ascii_whitespace() {
i += 1;
}
let value = if i < n && (b[i] == b'\'' || b[i] == b'"') {
let quote = b[i];
i += 1;
let mut v = String::new();
loop {
if i >= n {
return Err(if quote == b'\'' {
"the connection string has an unclosed single quote"
} else {
"the connection string has an unclosed double quote"
});
}
if b[i] == quote {
if i + 1 < n && b[i + 1] == quote {
v.push(quote as char);
i += 2;
continue;
}
i += 1;
break;
}
v.push(b[i] as char);
i += 1;
}
skip_trailing(b, &mut i, "unexpected characters after a quoted value")?;
v
} else if i < n && b[i] == b'{' {
i += 1;
let mut v = String::new();
loop {
if i >= n {
return Err("the connection string has an unclosed brace `{`");
}
if b[i] == b'}' {
i += 1;
break;
}
v.push(b[i] as char);
i += 1;
}
skip_trailing(
b,
&mut i,
"unexpected characters after a `{…}` value; quote with '' or \"\" if the value contains `}`",
)?;
v
} else {
let start = i;
while i < n && b[i] != b';' {
i += 1;
}
input[start..i]
.trim_end_matches(|c: char| c.is_ascii_whitespace())
.to_string()
};
pairs.insert(key, value);
if i < n && b[i] == b';' {
i += 1;
}
}
Ok(AdoNetString { pairs })
}
fn skip_trailing(b: &[u8], i: &mut usize, err: &'static str) -> Result<(), &'static str> {
while *i < b.len() && b[*i] != b';' {
if !b[*i].is_ascii_whitespace() {
return Err(err);
}
*i += 1;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::parse;
fn value(s: &str, key: &str) -> Option<String> {
parse(s)
.unwrap_or_else(|e| panic!("`{s}` failed to parse: {e}"))
.pairs()
.get(key)
.cloned()
}
#[test]
fn basic_pairs() {
let p = parse("Server=host;Database=db;User Id=sa;Password=pw").unwrap();
assert_eq!(p.pairs().get("server").map(String::as_str), Some("host"));
assert_eq!(p.pairs().get("database").map(String::as_str), Some("db"));
assert_eq!(p.pairs().get("user id").map(String::as_str), Some("sa"));
assert_eq!(p.pairs().get("password").map(String::as_str), Some("pw"));
}
#[test]
fn keys_are_lowercased_and_trimmed() {
assert_eq!(value(" SeRVeR =host", "server").as_deref(), Some("host"));
assert_eq!(value("USER ID=sa", "user id").as_deref(), Some("sa"));
}
#[test]
fn splits_each_pair_on_the_first_equals() {
assert_eq!(
value("Password=ab=cd", "password").as_deref(),
Some("ab=cd")
);
assert_eq!(
value("Password=Zm9vYmFy==", "password").as_deref(),
Some("Zm9vYmFy==")
);
assert_eq!(value("k=a=b=c=", "k").as_deref(), Some("a=b=c="));
}
#[test]
fn double_equals_in_key_is_a_literal_equals() {
assert!(parse("k==v").is_err());
assert_eq!(value("k='=v'", "k").as_deref(), Some("=v"));
assert_eq!(value("a==b=c", "a=b").as_deref(), Some("c"));
}
#[test]
fn empty_pairs_are_ignored() {
assert_eq!(value(";;Server=host;;", "server").as_deref(), Some("host"));
assert_eq!(value("Server=host;", "server").as_deref(), Some("host"));
assert_eq!(value(";Server=host", "server").as_deref(), Some("host"));
assert!(parse("").unwrap().pairs().is_empty());
assert!(parse(" ").unwrap().pairs().is_empty());
assert!(parse(";;;").unwrap().pairs().is_empty());
}
#[test]
fn whitespace_between_pairs_is_ignored() {
let p = parse("Server=host ; Database=db").unwrap();
assert_eq!(p.pairs().get("server").map(String::as_str), Some("host"));
assert_eq!(p.pairs().get("database").map(String::as_str), Some("db"));
}
#[test]
fn unquoted_values_are_trimmed_both_sides() {
assert_eq!(value("k= a b ", "k").as_deref(), Some("a b"));
assert_eq!(value("k=", "k").as_deref(), Some(""));
assert_eq!(value("k= ", "k").as_deref(), Some(""));
}
#[test]
fn quotes_preserve_surrounding_whitespace() {
assert_eq!(value("k=' a '", "k").as_deref(), Some(" a "));
assert_eq!(value("k=\" a \"", "k").as_deref(), Some(" a "));
assert_eq!(value("k=' '", "k").as_deref(), Some(" "));
}
#[test]
fn quotes_hold_structural_characters() {
assert_eq!(value("k='a;b=c{d'", "k").as_deref(), Some("a;b=c{d"));
assert_eq!(value("k=\"a;b=c{d\"", "k").as_deref(), Some("a;b=c{d"));
assert_eq!(value("k='a\"b'", "k").as_deref(), Some("a\"b"));
assert_eq!(value("k=\"a'b\"", "k").as_deref(), Some("a'b"));
}
#[test]
fn doubled_quote_embeds_the_quote() {
assert_eq!(value("k='a''b'", "k").as_deref(), Some("a'b"));
assert_eq!(value("k=\"a\"\"b\"", "k").as_deref(), Some("a\"b"));
assert_eq!(value("k=''''", "k").as_deref(), Some("'")); }
#[test]
fn braces_hold_structural_characters() {
assert_eq!(value("k={a;b=c}", "k").as_deref(), Some("a;b=c"));
assert_eq!(value("k={a'b\"c}", "k").as_deref(), Some("a'b\"c"));
}
#[test]
fn brace_cannot_contain_close_brace() {
assert_eq!(
parse("k={a}b}").unwrap_err(),
"unexpected characters after a `{…}` value; quote with '' or \"\" if the value contains `}`"
);
assert_eq!(value("k={a}", "k").as_deref(), Some("a"));
}
#[test]
fn trailing_junk_after_quote_is_rejected() {
assert_eq!(
parse("k='ab'x").unwrap_err(),
"unexpected characters after a quoted value"
);
assert_eq!(
parse("k=\"ab\"x").unwrap_err(),
"unexpected characters after a quoted value"
);
assert_eq!(value("k='ab' ;n=1", "k").as_deref(), Some("ab"));
}
#[test]
fn empty_quoted_value() {
assert_eq!(value("k=''", "k").as_deref(), Some(""));
assert_eq!(value("k=\"\"", "k").as_deref(), Some(""));
assert_eq!(value("k={}", "k").as_deref(), Some(""));
}
#[test]
fn ascii_control_bytes_are_not_treated_as_trim_whitespace() {
assert_eq!(
value("k=secret\u{000B}", "k").as_deref(),
Some("secret\u{000B}")
);
assert_eq!(value("k=secret\t ", "k").as_deref(), Some("secret"));
assert_eq!(value(" k =v", "k").as_deref(), Some("v"));
assert_eq!(value("k\u{000B}=v", "k\u{000B}").as_deref(), Some("v"));
}
#[test]
fn duplicate_keys_last_wins() {
assert_eq!(value("k=a;k=b;k=c", "k").as_deref(), Some("c"));
}
#[test]
fn server_value_passes_through_raw() {
assert_eq!(
value("Server=tcp:host.example.com,1433", "server").as_deref(),
Some("tcp:host.example.com,1433")
);
assert_eq!(
value("Server=host\\instance", "server").as_deref(),
Some("host\\instance")
);
}
#[test]
fn error_reasons() {
assert_eq!(
parse("=value").unwrap_err(),
"a key in the connection string is empty"
);
assert_eq!(
parse("k=a;b").unwrap_err(),
"key-value pairs must be joined by a `=`"
);
assert_eq!(
parse("k='ab").unwrap_err(),
"the connection string has an unclosed single quote"
);
assert_eq!(
parse("k=\"ab").unwrap_err(),
"the connection string has an unclosed double quote"
);
assert_eq!(
parse("k={ab").unwrap_err(),
"the connection string has an unclosed brace `{`"
);
assert!(parse("k=café").is_err());
}
#[test]
fn exhaustive_ascii_roundtrip_in_quotes() {
for byte in 0x20u8..=0x7e {
let c = byte as char;
let inner = if c == '\'' {
"a''b".to_string()
} else {
format!("a{c}b")
};
let expected = format!("a{c}b");
assert_eq!(
value(&format!("k='{inner}'"), "k").as_deref(),
Some(expected.as_str()),
"char {c:?} (0x{byte:02x}) via single quotes"
);
let inner = if c == '"' {
"a\"\"b".to_string()
} else {
format!("a{c}b")
};
assert_eq!(
value(&format!("k=\"{inner}\""), "k").as_deref(),
Some(expected.as_str()),
"char {c:?} (0x{byte:02x}) via double quotes"
);
}
}
#[test]
fn exhaustive_ascii_roundtrip_unquoted_where_legal() {
for byte in 0x21u8..=0x7e {
let c = byte as char;
if c == ';' {
continue; }
let raw = format!("a{c}b");
assert_eq!(
value(&format!("k={raw}"), "k").as_deref(),
Some(raw.as_str()),
"char {c:?} (0x{byte:02x}) unquoted"
);
}
}
}