mod nc;
use form_urlencoded::Serializer;
use indexmap::IndexMap;
use itertools::Either;
use miette::Diagnostic;
use std::iter::once;
use std::path::PathBuf;
use std::str::FromStr;
use thiserror::Error;
pub use nc::{parse, parse_glob};
#[derive(Debug)]
#[non_exhaustive]
pub struct Config {
pub database: Database,
pub database_prefix: String,
pub redis: RedisConfig,
pub notify_push_redis: Option<RedisConfig>,
pub nextcloud_url: String,
}
#[derive(Debug)]
pub enum RedisConfig {
Single(RedisConnectionInfo),
Cluster(RedisClusterConnectionInfo),
}
impl RedisConfig {
pub fn as_single(&self) -> Option<RedisConnectionInfo> {
match self {
RedisConfig::Single(single) => Some(single.clone()),
RedisConfig::Cluster(cluster) => cluster.iter().next(),
}
}
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Ord, Eq)]
pub enum RedisConnectionAddr {
Tcp { host: String, port: u16, tls: bool },
Unix { path: PathBuf },
}
impl RedisConnectionAddr {
fn parse(mut host: &str, port: Option<u16>, tls: bool) -> Self {
if host.starts_with("/") {
RedisConnectionAddr::Unix { path: host.into() }
} else {
let tls = if host.starts_with("tls://") || host.starts_with("rediss://") {
host = host.split_once("://").unwrap().1;
true
} else {
tls
};
if host == "localhost" {
host = "127.0.0.1";
}
let (host, port, _) = if let Some(port) = port {
(host, Some(port), None)
} else {
split_host(host)
};
RedisConnectionAddr::Tcp {
host: host.into(),
port: port.unwrap_or(6379),
tls,
}
}
}
}
#[derive(Clone, Debug)]
pub struct RedisClusterConnectionInfo {
pub addr: Vec<RedisConnectionAddr>,
pub db: i64,
pub username: Option<String>,
pub password: Option<String>,
pub tls_params: Option<RedisTlsParams>,
}
impl RedisClusterConnectionInfo {
pub fn iter(&self) -> impl Iterator<Item = RedisConnectionInfo> + '_ {
self.addr.iter().cloned().map(|addr| RedisConnectionInfo {
addr,
db: self.db,
username: self.username.clone(),
password: self.password.clone(),
tls_params: self.tls_params.clone(),
})
}
}
#[derive(Clone, Debug)]
pub struct RedisConnectionInfo {
pub addr: RedisConnectionAddr,
pub db: i64,
pub username: Option<String>,
pub password: Option<String>,
pub tls_params: Option<RedisTlsParams>,
}
#[derive(Clone, Debug, Default)]
pub struct RedisTlsParams {
pub local_cert: Option<PathBuf>,
pub local_pk: Option<PathBuf>,
pub ca_file: Option<PathBuf>,
pub accept_invalid_hostname: bool,
pub insecure: bool,
}
impl RedisConfig {
pub fn addr(&self) -> impl Iterator<Item = &RedisConnectionAddr> {
match self {
RedisConfig::Single(conn) => Either::Left(once(&conn.addr)),
RedisConfig::Cluster(cluster) => Either::Right(cluster.addr.iter()),
}
}
pub fn db(&self) -> i64 {
match self {
RedisConfig::Single(conn) => conn.db,
RedisConfig::Cluster(cluster) => cluster.db,
}
}
pub fn username(&self) -> Option<&str> {
match self {
RedisConfig::Single(conn) => conn.username.as_deref(),
RedisConfig::Cluster(cluster) => cluster.username.as_deref(),
}
}
pub fn passwd(&self) -> Option<&str> {
match self {
RedisConfig::Single(conn) => conn.password.as_deref(),
RedisConfig::Cluster(cluster) => cluster.password.as_deref(),
}
}
pub fn is_empty(&self) -> bool {
match self {
RedisConfig::Single(_) => false,
RedisConfig::Cluster(cluster) => cluster.addr.is_empty(),
}
}
}
type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, Error, Diagnostic)]
pub enum Error {
#[error(transparent)]
#[diagnostic(transparent)]
Php(PhpParseError),
#[error("Provided config file doesn't seem to be a nextcloud config file: {0:#}")]
NotAConfig(#[from] NotAConfigError),
#[error("Failed to read config file")]
ReadFailed(std::io::Error, PathBuf),
#[error("invalid database configuration: {0}")]
InvalidDb(#[from] DbError),
#[error("Invalid redis configuration")]
Redis,
#[error("`overwrite.cli.url` not set`")]
NoUrl,
}
#[derive(Debug, Error, Diagnostic)]
#[error("Error while parsing '{path}':\n{err}")]
#[diagnostic(forward(err))]
pub struct PhpParseError {
err: php_literal_parser::ParseError,
path: PathBuf,
}
#[derive(Debug, Error)]
pub enum DbError {
#[error("unsupported database type {0}")]
Unsupported(String),
#[error("no username set")]
NoUsername,
#[error("no password set")]
NoPassword,
#[error("no data directory")]
NoDataDirectory,
}
#[derive(Debug, Error)]
pub enum NotAConfigError {
#[error("$CONFIG not found in file")]
NoConfig(PathBuf),
#[error("$CONFIG is not an array")]
NotAnArray(PathBuf),
}
#[derive(Debug, Clone, PartialEq)]
pub enum SslOptions {
Enabled {
key: String,
cert: String,
ca: String,
verify: bool,
},
Disabled,
Default,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Database {
Sqlite {
database: PathBuf,
},
MySql {
database: String,
username: String,
password: String,
connect: DbConnect,
ssl_options: SslOptions,
},
Postgres {
database: String,
username: String,
password: String,
connect: DbConnect,
options: IndexMap<String, String>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum DbConnect {
Tcp { host: String, port: u16 },
Socket(PathBuf),
}
impl Database {
pub fn url(&self) -> String {
match self {
Database::Sqlite { database } => {
format!("sqlite://{}", database.display())
}
Database::MySql {
database,
username,
password,
connect,
ssl_options,
} => {
let mut params = Serializer::new(String::new());
match ssl_options {
SslOptions::Default => {}
SslOptions::Disabled => {
params.append_pair("ssl-mode", "disabled");
}
SslOptions::Enabled { ca, verify, .. } => {
params.append_pair(
"ssl-mode",
if *verify {
"verify_identity"
} else {
"verify_ca"
},
);
params.append_pair("ssl-ca", ca.as_str());
}
}
let (host, port) = match connect {
DbConnect::Socket(socket) => {
params.append_pair("socket", &socket.to_string_lossy());
("localhost", 3306) }
DbConnect::Tcp { host, port } => (host.as_str(), *port),
};
let params = params.finish().replace("%2F", "/");
let params_start = if params.is_empty() { "" } else { "?" };
if port == 3306 {
format!(
"mysql://{}:{}@{}/{}{}{}",
urlencoding::encode(username),
urlencoding::encode(password),
host,
database,
params_start,
params
)
} else {
format!(
"mysql://{}:{}@{}:{}/{}{}{}",
urlencoding::encode(username),
urlencoding::encode(password),
host,
port,
database,
params_start,
params
)
}
}
Database::Postgres {
database,
username,
password,
connect,
options,
} => {
let mut params = Serializer::new(String::new());
for (key, value) in options {
params.append_pair(key.as_str(), value.as_str());
}
let (host, port) = match connect {
DbConnect::Socket(socket) => {
params.append_pair("host", &socket.to_string_lossy());
("localhost", 5432) }
DbConnect::Tcp { host, port } => (host.as_str(), *port),
};
let params = params.finish().replace("%2F", "/");
let params_start = if params.is_empty() { "" } else { "?" };
if port == 5432 {
format!(
"postgresql://{}:{}@{}/{}{}{}",
urlencoding::encode(username),
urlencoding::encode(password),
host,
database,
params_start,
params
)
} else {
format!(
"postgresql://{}:{}@{}:{}/{}{}{}",
urlencoding::encode(username),
urlencoding::encode(password),
host,
port,
database,
params_start,
params
)
}
}
}
}
}
fn split_host(host: &str) -> (&str, Option<u16>, Option<&str>) {
if host.starts_with('/') {
return ("localhost", None, Some(host));
}
let (host, port_or_socket) = if host.starts_with('[') {
if let Some(pos) = host.rfind("]:") {
(&host[0..pos + 1], &host[pos + 2..])
} else {
(host, "")
}
} else {
host.rsplit_once(':').unwrap_or((host, ""))
};
if port_or_socket.is_empty() {
return (host, None, None);
}
match u16::from_str(port_or_socket) {
Ok(port) => (host, Some(port), None),
Err(_) => (host, None, Some(port_or_socket)),
}
}
#[test]
fn test_spit_host() {
assert_eq!(("localhost", None, None), split_host("localhost"));
assert_eq!(("localhost", Some(123), None), split_host("localhost:123"));
assert_eq!(
("localhost", None, Some("foo")),
split_host("localhost:foo")
);
assert_eq!(("[::1]", None, None), split_host("[::1]"));
assert_eq!(("[::1]", Some(123), None), split_host("[::1]:123"));
}