use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use crate::{bail, ensure};
#[derive(Debug)]
pub struct AdoNetString {
pairs: HashMap<String, String>,
}
impl Deref for AdoNetString {
type Target = HashMap<String, String>;
fn deref(&self) -> &Self::Target {
&self.pairs
}
}
impl DerefMut for AdoNetString {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.pairs
}
}
impl FromStr for AdoNetString {
type Err = crate::Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut lexer = Lexer::tokenize(input)?;
let mut pairs = HashMap::new();
for n in 0.. {
if lexer.peek().kind() == &TokenKind::Eof {
break;
}
if n != 0 {
let err = "Key-value pairs must be separated by a `;`";
ensure!(lexer.next().kind() == &TokenKind::Semi, err);
if lexer.peek().kind() == &TokenKind::Eof {
break;
}
}
let key = read_ident(&mut lexer)?;
ensure!(!key.is_empty(), "Key must not be empty");
let err = "key-value pairs must be joined by a `=`";
ensure!(lexer.next().kind() == &TokenKind::Eq, err);
let value = read_ident(&mut lexer)?;
let key = key.to_lowercase();
pairs.insert(key, value);
}
Ok(Self { pairs })
}
}
fn read_ident(lexer: &mut Lexer) -> crate::Result<String> {
let mut output = String::new();
loop {
let Token { kind, .. } = lexer.peek();
match kind {
TokenKind::Atom(c) => {
let _ = lexer.next();
output.push(c);
}
TokenKind::Escaped(seq) => {
let _ = lexer.next();
output.extend(seq);
}
TokenKind::Semi => break,
TokenKind::Eq => break,
TokenKind::Newline => {
let _ = lexer.next();
continue;
}
TokenKind::Whitespace => {
let _ = lexer.next();
match output.len() {
0 => continue,
_ => output.push(' '),
}
}
TokenKind::Eof => break,
}
}
output = output.trim_end().to_owned();
Ok(output)
}
#[derive(Debug, Clone)]
struct Token {
kind: TokenKind,
loc: Location,
}
impl Token {
fn new(kind: TokenKind, loc: Location) -> Self {
Self { kind, loc }
}
fn kind(&self) -> &TokenKind {
&self.kind
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
enum TokenKind {
Semi,
Eq,
Atom(char),
Escaped(Vec<char>),
Newline,
Whitespace,
Eof,
}
#[derive(Debug)]
struct Lexer {
tokens: Vec<Token>,
}
impl Lexer {
fn tokenize(mut input: &str) -> crate::Result<Self> {
let mut tokens = vec![];
let mut loc = Location::default();
while !input.is_empty() {
let old_input = input;
let mut chars = input.chars();
let kind = match chars.next().unwrap() {
'"' => {
let mut buf = Vec::new();
loop {
match chars.next() {
None => bail!("unclosed double quote"),
Some('"') => match lookahead(&chars) {
Some('"') => {
if buf.len() == 0 {
break;
}
let _ = chars.next();
buf.push('"');
buf.push('"');
}
Some(_) | None => break,
},
Some(c) if c.is_ascii() => buf.push(c),
_ => bail!("Invalid ado.net token"),
}
}
TokenKind::Escaped(buf)
}
'\'' => {
let mut buf = Vec::new();
loop {
match chars.next() {
None => bail!("unclosed single quote"),
Some('\'') => match lookahead(&chars) {
Some('\'') => {
if buf.len() == 0 {
break;
}
let _ = chars.next();
buf.push('\'');
buf.push('\'');
}
Some(_) | None => break,
},
Some(c) if c.is_ascii() => buf.push(c),
Some(c) => bail!("Invalid ado.net token `{}`", c),
}
}
TokenKind::Escaped(buf)
}
'{' => {
let mut buf = Vec::new();
loop {
match chars.next() {
None => bail!("unclosed escape literal"),
Some('}') => break,
Some(c) if c.is_ascii() => buf.push(c),
Some(c) => bail!("Invalid ado.net token `{}`", c),
}
}
TokenKind::Escaped(buf)
}
';' => TokenKind::Semi,
'=' => TokenKind::Eq,
'\n' => TokenKind::Newline,
' ' => TokenKind::Whitespace,
char if char.is_ascii() => TokenKind::Atom(char),
char => bail!("Invalid character found: {}", char),
};
tokens.push(Token::new(kind, loc));
input = chars.as_str();
let consumed = old_input.len() - input.len();
loc.advance(&old_input[..consumed]);
}
tokens.reverse();
Ok(Self { tokens })
}
#[must_use]
pub(crate) fn next(&mut self) -> Token {
self.tokens.pop().unwrap_or(Token {
kind: TokenKind::Eof,
loc: Location::default(),
})
}
#[must_use]
pub(crate) fn peek(&mut self) -> Token {
self.tokens.last().map(|t| t.clone()).unwrap_or(Token {
kind: TokenKind::Eof,
loc: Location::default(),
})
}
}
fn lookahead(iter: &std::str::Chars<'_>) -> Option<char> {
let s = iter.as_str();
s.chars().next()
}
#[derive(Copy, Clone, Default, Debug)]
pub(crate) struct Location {
pub(crate) column: usize,
}
impl Location {
fn advance(&mut self, text: &str) {
self.column += text.chars().count();
}
}
#[cfg(test)]
mod test {
use super::AdoNetString;
fn assert_kv(ado: &AdoNetString, key: &str, value: &str) {
assert_eq!(ado.get(&key.to_lowercase()), Some(&value.to_owned()));
}
#[test]
fn windows_auth_with_sql_client() -> crate::Result<()> {
let input = "Persist Security Info=False;Integrated Security=true;\nInitial Catalog=AdventureWorks;Server=MSSQL1";
let ado: AdoNetString = input.parse()?;
assert_kv(&ado, "Persist Security Info", "False");
assert_kv(&ado, "Integrated Security", "true");
assert_kv(&ado, "Server", "MSSQL1");
assert_kv(&ado, "Initial Catalog", "AdventureWorks");
Ok(())
}
#[test]
fn sql_server_auth_with_sql_client() -> crate::Result<()> {
let input = "Persist Security Info=False;User ID=*****;Password=*****;Initial Catalog=AdventureWorks;Server=MySqlServer";
let ado: AdoNetString = input.parse()?;
assert_kv(&ado, "Persist Security Info", "False");
assert_kv(&ado, "User ID", "*****");
assert_kv(&ado, "Password", "*****");
assert_kv(&ado, "Initial Catalog", "AdventureWorks");
assert_kv(&ado, "Server", "MySqlServer");
Ok(())
}
#[test]
fn connect_to_named_sql_server_instance() -> crate::Result<()> {
let input = r#"Data Source=MySqlServer\MSSQL1;"#;
let ado: AdoNetString = input.parse()?;
assert_kv(&ado, "Data Source", r#"MySqlServer\MSSQL1"#);
Ok(())
}
#[test]
fn oledb_connection_string_syntax() -> crate::Result<()> {
let input = r#"Provider=Microsoft.Jet.OLEDB.4.0; Data Source=d:\Northwind.mdb;User ID=Admin;Password=;"#;
let ado: AdoNetString = input.parse()?;
assert_kv(&ado, "Provider", r#"Microsoft.Jet.OLEDB.4.0"#);
assert_kv(&ado, "Data Source", r#"d:\Northwind.mdb"#);
assert_kv(&ado, "User ID", r#"Admin"#);
assert_kv(&ado, "Password", r#""#);
let input = r#"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=d:\Northwind.mdb;Jet OLEDB:System Database=d:\NorthwindSystem.mdw;User ID=*****;Password=*****;"#;
let ado: AdoNetString = input.parse()?;
assert_kv(&ado, "Provider", r#"Microsoft.Jet.OLEDB.4.0"#);
assert_kv(&ado, "Data Source", r#"d:\Northwind.mdb"#);
assert_kv(
&ado,
"Jet OLEDB:System Database",
r#"d:\NorthwindSystem.mdw"#,
);
assert_kv(&ado, "User ID", r#"*****"#);
assert_kv(&ado, "Password", r#"*****"#);
Ok(())
}
#[test]
fn connect_to_access_jet() -> crate::Result<()> {
let input = r#"Provider=Microsoft.Jet.OLEDB.4.0;
Data Source=|DataDirectory|\Northwind.mdb;
Jet OLEDB:System Database=|DataDirectory|\System.mdw;"#;
let ado: AdoNetString = input.parse()?;
assert_kv(&ado, "Data Source", r#"|DataDirectory|\Northwind.mdb"#);
assert_kv(&ado, "Provider", r#"Microsoft.Jet.OLEDB.4.0"#);
assert_kv(
&ado,
"Jet OLEDB:System Database",
r#"|DataDirectory|\System.mdw"#,
);
Ok(())
}
#[test]
fn data_shape_provider() -> crate::Result<()> {
let input = r#"Provider=MSDataShape;Data Provider=SQLOLEDB;Data Source=(local);Initial Catalog=pubs;Integrated Security=SSPI;"#;
let ado: AdoNetString = input.parse()?;
assert_kv(&ado, "Provider", r#"MSDataShape"#);
assert_kv(&ado, "Data Provider", r#"SQLOLEDB"#);
assert_kv(&ado, "Data Source", r#"(local)"#);
assert_kv(&ado, "Initial Catalog", r#"pubs"#);
assert_kv(&ado, "Integrated Security", r#"SSPI"#);
Ok(())
}
#[test]
fn odbc_connection_strings() -> crate::Result<()> {
let input = r#"Driver={Microsoft Text Driver (*.txt; *.csv)};DBQ=d:\bin"#;
let ado: AdoNetString = input.parse()?;
assert_kv(&ado, "Driver", r#"Microsoft Text Driver (*.txt; *.csv)"#);
assert_kv(&ado, "DBQ", r#"d:\bin"#);
Ok(())
}
#[test]
fn oracle_connection_strings() -> crate::Result<()> {
let input = "Data Source=Oracle9i;User ID=*****;Password=*****;";
let ado: AdoNetString = input.parse()?;
assert_kv(&ado, "Data Source", "Oracle9i");
assert_kv(&ado, "User ID", "*****");
assert_kv(&ado, "Password", "*****");
Ok(())
}
}