use std::collections::HashMap;
use std::str::FromStr;
use crate::{bail, ensure};
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct JdbcString {
sub_protocol: String,
server_name: Option<String>,
instance_name: Option<String>,
port: Option<u16>,
properties: HashMap<String, String>,
}
impl JdbcString {
pub fn sub_protocol(&self) -> &str {
&self.sub_protocol
}
pub fn server_name(&self) -> Option<&str> {
self.server_name.as_ref().map(|s| s.as_str())
}
pub fn instance_name(&self) -> Option<&str> {
self.instance_name.as_ref().map(|s| s.as_str())
}
pub fn port(&self) -> Option<u16> {
self.port
}
pub fn properties(&self) -> &HashMap<String, String> {
&self.properties
}
pub fn properties_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.properties
}
}
impl FromStr for JdbcString {
type Err = crate::Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut lexer = Lexer::tokenize(input)?;
let err = "Invalid JDBC sub-protocol";
cmp_str(&mut lexer, "jdbc", err)?;
ensure!(lexer.next().kind() == &TokenKind::Colon, err);
let sub_protocol = format!("jdbc:{}", read_ident(&mut lexer, err)?);
ensure!(lexer.next().kind() == &TokenKind::Colon, err);
ensure!(lexer.next().kind() == &TokenKind::FSlash, err);
ensure!(lexer.next().kind() == &TokenKind::FSlash, err);
let mut server_name = None;
if matches!(lexer.peek().kind(), TokenKind::Atom(_) | TokenKind::Escaped(_)) {
server_name = Some(read_ident(&mut lexer, "Invalid server name")?);
}
let mut instance_name = None;
if matches!(lexer.peek().kind(), TokenKind::BSlash) {
let _ = lexer.next();
instance_name = Some(read_ident(&mut lexer, "Invalid instance name")?);
}
let mut port = None;
if matches!(lexer.peek().kind(), TokenKind::Colon) {
let _ = lexer.next();
let err = "Invalid port";
let s = read_ident(&mut lexer, err)?;
port = Some(s.parse()?);
}
let mut properties = HashMap::new();
while let TokenKind::Semi = lexer.peek().kind() {
let _ = lexer.next();
if let TokenKind::Eof = lexer.peek().kind() {
let _ = lexer.next();
break;
}
let err = "Invalid property key";
let key = read_ident(&mut lexer, err)?.to_lowercase();
let err = "Property pairs must be joined by a `=`";
ensure!(lexer.next().kind() == &TokenKind::Eq, err);
let err = "Invalid property value";
let value = read_ident(&mut lexer, err)?;
properties.insert(key, value);
}
let token = lexer.next();
ensure!(token.kind() == &TokenKind::Eof, "Invalid JDBC token");
Ok(Self {
sub_protocol,
server_name,
instance_name,
port,
properties,
})
}
}
fn cmp_str(lexer: &mut Lexer, s: &str, err_msg: &'static str) -> crate::Result<()> {
for char in s.chars() {
if let Token {
kind: TokenKind::Atom(tchar),
..
} = lexer.next()
{
ensure!(char == tchar, err_msg);
} else {
bail!(err_msg);
}
}
Ok(())
}
fn read_ident(lexer: &mut Lexer, err_msg: &'static str) -> crate::Result<String> {
let mut output = String::new();
loop {
let token = lexer.next();
match token.kind() {
TokenKind::Escaped(seq) => output.extend(seq),
TokenKind::Atom(c) => output.push(*c),
_ => {
lexer.push(token);
break;
}
}
}
match output.len() {
0 => bail!(err_msg),
_ => Ok(output),
}
}
#[derive(Debug)]
struct Lexer {
tokens: Vec<Token>,
}
impl Lexer {
pub(crate) 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() {
':' => TokenKind::Colon,
'=' => TokenKind::Eq,
'\\' => TokenKind::BSlash,
'/' => TokenKind::FSlash,
';' => TokenKind::Semi,
'{' => {
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 JDBC token `{}`", c),
}
}
TokenKind::Escaped(buf)
}
c if c.is_ascii() => TokenKind::Atom(c),
c => bail!("Invalid JDBC token `{}`", c),
};
tokens.push(Token { 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(),
})
}
pub(crate) fn push(&mut self, token: Token) {
self.tokens.push(token);
}
#[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(),
})
}
}
#[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();
}
}
#[derive(Debug, Clone)]
struct Token {
loc: Location,
kind: TokenKind,
}
impl Token {
pub(crate) fn kind(&self) -> &TokenKind {
&self.kind
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
enum TokenKind {
Colon,
Eq,
BSlash,
FSlash,
Semi,
Escaped(Vec<char>),
Atom(char),
Eof,
}
#[cfg(test)]
mod test {
use super::JdbcString;
#[test]
fn parse_sub_protocol() -> crate::Result<()> {
let conn: JdbcString = "jdbc:sqlserver://".parse()?;
assert_eq!(conn.sub_protocol(), "jdbc:sqlserver");
Ok(())
}
#[test]
fn parse_server_name() -> crate::Result<()> {
let conn: JdbcString = r#"jdbc:sqlserver://server"#.parse()?;
assert_eq!(conn.sub_protocol(), "jdbc:sqlserver");
assert_eq!(conn.server_name(), Some("server"));
Ok(())
}
#[test]
fn parse_instance_name() -> crate::Result<()> {
let conn: JdbcString = r#"jdbc:sqlserver://server\instance"#.parse()?;
assert_eq!(conn.sub_protocol(), "jdbc:sqlserver");
assert_eq!(conn.server_name(), Some("server"));
assert_eq!(conn.instance_name(), Some("instance"));
Ok(())
}
#[test]
fn parse_port() -> crate::Result<()> {
let conn: JdbcString = r#"jdbc:sqlserver://server\instance:80"#.parse()?;
assert_eq!(conn.sub_protocol(), "jdbc:sqlserver");
assert_eq!(conn.server_name(), Some("server"));
assert_eq!(conn.instance_name(), Some("instance"));
assert_eq!(conn.port(), Some(80));
Ok(())
}
#[test]
fn parse_properties() -> crate::Result<()> {
let conn: JdbcString =
r#"jdbc:sqlserver://server\instance:80;key=value;foo=bar"#.parse()?;
assert_eq!(conn.sub_protocol(), "jdbc:sqlserver");
assert_eq!(conn.server_name(), Some("server"));
assert_eq!(conn.instance_name(), Some("instance"));
assert_eq!(conn.port(), Some(80));
let kv = conn.properties();
assert_eq!(kv.get("foo"), Some(&"bar".to_string()));
assert_eq!(kv.get("key"), Some(&"value".to_string()));
Ok(())
}
#[test]
fn escaped_properties() -> crate::Result<()> {
let conn: JdbcString =
r#"jdbc:sqlserver://se{r}ver{;}\instance:80;key={va[]}lue"#.parse()?;
assert_eq!(conn.sub_protocol(), "jdbc:sqlserver");
assert_eq!(conn.server_name(), Some("server;"));
assert_eq!(conn.instance_name(), Some("instance"));
assert_eq!(conn.port(), Some(80));
let kv = conn.properties();
assert_eq!(kv.get("key"), Some(&"va[]lue".to_string()));
Ok(())
}
#[test]
fn sub_protocol_error() -> crate::Result<()> {
let err = r#"jdbq:sqlserver://"#.parse::<JdbcString>().unwrap_err().to_string();
assert_eq!(
err.to_string(),
"Conversion error: Invalid JDBC sub-protocol"
);
Ok(())
}
#[test]
fn whitespace() -> crate::Result<()> {
let conn: JdbcString =
r#"jdbc:sqlserver://server\instance:80;key=value;foo=bar;user id=musti naukio"#
.parse()?;
assert_eq!(conn.sub_protocol(), "jdbc:sqlserver");
assert_eq!(conn.server_name(), Some(r#"server"#));
assert_eq!(conn.instance_name(), Some("instance"));
assert_eq!(conn.port(), Some(80));
let kv = conn.properties();
assert_eq!(kv.get("user id"), Some(&"musti naukio".to_string()));
Ok(())
}
#[test]
fn regression_2020_10_06() -> crate::Result<()> {
let input = "jdbc:sqlserver://my-server.com:5433;foo=bar";
let _conn: JdbcString = input.parse()?;
let input = "jdbc:oracle://foo.bar:1234";
let _conn: JdbcString = input.parse()?;
Ok(())
}
#[test]
fn regression_2020_10_07_handle_trailing_semis() -> crate::Result<()> {
let input = "jdbc:sqlserver://my-server.com:5433;foo=bar;";
let _conn: JdbcString = input.parse()?;
let input = "jdbc:sqlserver://my-server.com:4200;User ID=musti;Password={abc;}}45}";
let conn: JdbcString = input.parse()?;
let props = conn.properties();
assert_eq!(props.get("user id"), Some(&"musti".to_owned()));
assert_eq!(props.get("password"), Some(&"abc;}45}".to_owned()));
Ok(())
}
}