#![forbid(unsafe_code)]
pub mod connectstring;
#[cfg(feature = "cassette")]
pub mod cassette;
use crate::{ProtocolError, Result};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
pub enum Protocol {
#[default]
Tcp,
Tcps,
}
impl Protocol {
#[must_use]
pub fn default_port(self) -> u16 {
match self {
Self::Tcp => 1521,
Self::Tcps => 2484,
}
}
#[must_use]
pub fn is_tls(self) -> bool {
matches!(self, Self::Tcps)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EasyConnect {
pub host: String,
pub port: u16,
pub service_name: String,
pub protocol: Protocol,
}
impl From<connectstring::Protocol> for Protocol {
fn from(value: connectstring::Protocol) -> Self {
match value {
connectstring::Protocol::Tcp => Self::Tcp,
connectstring::Protocol::Tcps => Self::Tcps,
}
}
}
impl EasyConnect {
pub fn parse(input: &str) -> Result<Self> {
let descriptor = connectstring::parse(input)?.ok_or_else(|| {
ProtocolError::InvalidConnectDescriptor(format!(
"\"{input}\" is not a connect descriptor or EZConnect string \
(it may be a tnsnames.ora alias requiring a config directory)"
))
})?;
let address = descriptor.first_address().ok_or_else(|| {
ProtocolError::InvalidConnectDescriptor(
"connect descriptor defines no usable address (host is required)".to_string(),
)
})?;
let host = address.host.clone().ok_or_else(|| {
ProtocolError::InvalidConnectDescriptor("host is required".to_string())
})?;
let service_name = descriptor
.first_description()
.connect_data
.service_name
.clone()
.ok_or_else(|| {
ProtocolError::InvalidConnectDescriptor("service name is required".to_string())
})?;
Ok(Self {
host,
port: address.port,
service_name,
protocol: address.protocol.into(),
})
}
pub fn parse_descriptor(input: &str) -> Result<connectstring::Descriptor> {
connectstring::parse(input)?.ok_or_else(|| {
ProtocolError::InvalidConnectDescriptor(format!(
"\"{input}\" is not a connect descriptor or EZConnect string"
))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_easy_connect_with_default_port() {
let parsed = EasyConnect::parse("localhost/FREEPDB1")
.expect("default-port EZConnect descriptor should parse");
assert_eq!(parsed.host, "localhost");
assert_eq!(parsed.port, 1521);
assert_eq!(parsed.service_name, "FREEPDB1");
}
#[test]
fn parses_easy_connect_with_explicit_port() {
let parsed = EasyConnect::parse("db.example.test:1522/service")
.expect("explicit-port EZConnect descriptor should parse");
assert_eq!(parsed.host, "db.example.test");
assert_eq!(parsed.port, 1522);
assert_eq!(parsed.service_name, "service");
assert_eq!(parsed.protocol, Protocol::Tcp);
}
#[test]
fn parses_tcps_prefix_defaults_to_2484() {
let parsed = EasyConnect::parse("tcps://db.example.test/FREEPDB1")
.expect("tcps EZConnect descriptor should parse");
assert_eq!(parsed.host, "db.example.test");
assert_eq!(parsed.port, 2484);
assert_eq!(parsed.service_name, "FREEPDB1");
assert_eq!(parsed.protocol, Protocol::Tcps);
assert!(parsed.protocol.is_tls());
}
#[test]
fn parses_tcps_prefix_with_explicit_port() {
let parsed = EasyConnect::parse("tcps://host:2484/svc").expect("should parse");
assert_eq!(parsed.port, 2484);
assert_eq!(parsed.protocol, Protocol::Tcps);
}
#[test]
fn parses_tcp_prefix_explicitly() {
let parsed = EasyConnect::parse("tcp://host/svc").expect("should parse");
assert_eq!(parsed.port, 1521);
assert_eq!(parsed.protocol, Protocol::Tcp);
}
#[test]
fn parses_full_tns_descriptor_via_easy_connect() {
let parsed = EasyConnect::parse(
"(DESCRIPTION=(ADDRESS=(PROTOCOL=tcps)(HOST=db.example.test)(PORT=2484))\
(CONNECT_DATA=(SERVICE_NAME=FREEPDB1)))",
)
.expect("full TNS descriptor should parse via EasyConnect");
assert_eq!(parsed.host, "db.example.test");
assert_eq!(parsed.port, 2484);
assert_eq!(parsed.service_name, "FREEPDB1");
assert_eq!(parsed.protocol, Protocol::Tcps);
}
#[test]
fn picks_first_address_of_address_list() {
let parsed = EasyConnect::parse(
"(DESCRIPTION=(ADDRESS_LIST=\
(ADDRESS=(PROTOCOL=tcp)(HOST=primary)(PORT=1521))\
(ADDRESS=(PROTOCOL=tcp)(HOST=standby)(PORT=1522)))\
(CONNECT_DATA=(SERVICE_NAME=svc)))",
)
.expect("address-list descriptor should parse");
assert_eq!(parsed.host, "primary");
assert_eq!(parsed.port, 1521);
}
}