mod address;
mod credentials;
mod protocol_version;
use crate::error::ConnectionError;
use address::AddressList;
pub use credentials::{Credentials, LoginKind};
use lazy_regex::regex;
pub use protocol_version::ProtocolVersion;
use regex::Captures;
use rsa::RsaPublicKey;
use serde::ser::{Serialize, SerializeMap, Serializer};
use serde_json::json;
use std::collections::HashMap;
use std::env;
const FIXED_CON_OPTS_LEN: usize = 7;
type ConResult<T> = std::result::Result<T, ConnectionError>;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ConOpts {
dsn: Option<String>,
login_kind: LoginKind,
schema: Option<String>,
port: u16,
protocol_version: ProtocolVersion,
client_name: String,
client_version: String,
client_os: String,
fetch_size: usize,
query_timeout: u64,
use_encryption: bool,
use_compression: bool,
lowercase_columns: bool,
autocommit: bool,
}
impl Default for ConOpts {
fn default() -> Self {
let crate_version = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "UNKNOWN".to_owned());
let encryption = cfg!(any(feature = "native-tls-basic", feature = "rustls"));
Self {
dsn: None,
login_kind: LoginKind::default(),
schema: None,
port: 8563,
protocol_version: ProtocolVersion::V3,
client_name: format!("{} {}", "Rust Exasol", crate_version),
client_version: crate_version,
client_os: env::consts::OS.to_owned(),
fetch_size: 5 * 1024 * 1024,
query_timeout: 0,
use_encryption: encryption,
use_compression: false,
lowercase_columns: true,
autocommit: true,
}
}
}
impl ConOpts {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn dsn(&self) -> Option<&str> {
self.dsn.as_deref()
}
#[inline]
pub fn set_dsn<T>(&mut self, dsn: T)
where
T: Into<String>,
{
self.dsn = Some(dsn.into())
}
#[inline]
pub fn port(&self) -> u16 {
self.port
}
#[inline]
pub fn set_port(&mut self, port: u16) {
self.port = port
}
#[inline]
pub fn schema(&self) -> Option<&str> {
self.schema.as_deref()
}
#[inline]
pub fn set_schema<T>(&mut self, schema: Option<T>)
where
T: Into<String>,
{
self.schema = schema.map(|s| s.into())
}
#[inline]
pub fn login_kind(&self) -> &LoginKind {
&self.login_kind
}
#[inline]
pub fn set_login_kind(&mut self, creds: LoginKind) {
self.login_kind = creds
}
#[inline]
pub fn protocol_version(&self) -> ProtocolVersion {
self.protocol_version
}
#[inline]
pub fn set_protocol_version(&mut self, pv: ProtocolVersion) {
self.protocol_version = pv
}
#[inline]
pub fn fetch_size(&self) -> usize {
self.fetch_size
}
#[inline]
pub fn set_fetch_size(&mut self, fetch_size: usize) {
self.fetch_size = fetch_size
}
#[inline]
pub fn query_timeout(&self) -> u64 {
self.query_timeout
}
#[inline]
pub fn set_query_timeout(&mut self, timeout: u64) {
self.query_timeout = timeout
}
#[inline]
pub fn encryption(&self) -> bool {
self.use_encryption
}
#[inline]
#[allow(unused)]
pub fn set_encryption(&mut self, flag: bool) {
if cfg!(any(feature = "native-tls-basic", feature = "rustls")) {
self.use_encryption = flag;
} else {
panic!("native-tls or rustls features must be enabled to set encryption")
}
}
#[inline]
pub fn compression(&self) -> bool {
self.use_compression
}
#[inline]
#[allow(unused)]
pub fn set_compression(&mut self, flag: bool) {
if cfg!(feature = "flate2") {
self.use_compression = flag;
} else {
panic!("flate2 feature must be enabled to set compression")
}
}
#[inline]
pub fn lowercase_columns(&self) -> bool {
self.lowercase_columns
}
#[inline]
pub fn set_lowercase_columns(&mut self, flag: bool) {
self.lowercase_columns = flag;
}
#[inline]
pub fn autocommit(&self) -> bool {
self.autocommit
}
#[inline]
pub fn set_autocommit(&mut self, flag: bool) {
self.autocommit = flag
}
#[inline]
pub(crate) fn encrypt_password(&mut self, key: RsaPublicKey) -> ConResult<()> {
self.login_kind.encrypt_password(key)
}
#[inline]
pub(crate) fn ws_prefix(&self) -> &str {
match self.encryption() {
false => "ws",
true => "wss",
}
}
pub(crate) fn parse_dsn(&self) -> ConResult<AddressList> {
let re = regex!(
r"(?x)
^(.+?) # Hostname prefix
(?:(\d+)\.\.(\d+)(.*?))? # Optional range and hostname suffix (e.g. hostname1..4.com)
(?:/([0-9A-Fa-f]+))? # Optional fingerprint (e.g. hostname1..4.com/135a1d2dce102de866f58267521f4232153545a075dc85f8f7596f57e588a181)
(?::(\d+)?)?$ # Optional port (e.g. hostname1..4.com:8564)
"
);
self.dsn
.as_deref()
.and_then(|dsn| re.captures(dsn))
.ok_or(ConnectionError::InvalidDSN)
.and_then(|cap| {
let hostname_prefix = &cap[1];
let start_range = Self::parse_dsn_part(&cap, 2);
let end_range = Self::parse_dsn_part(&cap, 3);
let hostname_suffix = Self::parse_dsn_part(&cap, 4);
let fingerprint = Self::parse_dsn_part(&cap, 5).to_owned();
let port = Self::parse_dsn_part(&cap, 6)
.parse::<u16>()
.unwrap_or(self.port);
let mut hosts = Vec::new();
if start_range.is_empty() {
hosts.push(format!("{}{}:{}", hostname_prefix, hostname_suffix, port));
} else {
let start_range = start_range.parse::<u8>()?;
let end_range = end_range.parse::<u8>()?;
for i in start_range..end_range {
hosts.push(format!(
"{}{}{}:{}",
hostname_prefix, i, hostname_suffix, port
))
}
}
AddressList::new(hosts, port, fingerprint)
})
}
#[inline]
fn parse_dsn_part<'a>(cap: &'a Captures, index: usize) -> &'a str {
cap.get(index).map_or("", |s| s.as_str())
}
}
impl Serialize for ConOpts {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let creds_len = match self.login_kind() {
LoginKind::Credentials(_) => 2,
_ => 1,
};
let attr = HashMap::from([
("currentSchema", json!(self.schema().unwrap_or_default())),
("autocommit", json!(self.autocommit())),
("queryTimeout", json!(self.query_timeout())),
]);
let mut opts = serializer.serialize_map(Some(FIXED_CON_OPTS_LEN + creds_len))?;
match self.login_kind() {
LoginKind::AccessToken(token) => opts.serialize_entry("accessToken", token)?,
LoginKind::RefreshToken(token) => opts.serialize_entry("refreshToken", token)?,
LoginKind::Credentials(cr) => {
opts.serialize_entry("username", cr.username())?;
opts.serialize_entry("password", cr.password())?;
}
}
opts.serialize_entry("driverName", self.client_name.as_str())?;
opts.serialize_entry("clientName", self.client_name.as_str())?;
opts.serialize_entry("clientVersion", self.client_version.as_str())?;
opts.serialize_entry("clientOs", self.client_os.as_str())?;
opts.serialize_entry("clientRuntime", "Rust")?;
opts.serialize_entry("useCompression", &self.compression())?;
opts.serialize_entry("attributes", &attr)?;
opts.end()
}
}