nextcloud_config_parser/
lib.rs

1mod nc;
2
3use form_urlencoded::Serializer;
4use itertools::Either;
5use miette::Diagnostic;
6use std::iter::once;
7use std::path::PathBuf;
8use std::str::FromStr;
9use thiserror::Error;
10
11pub use nc::{parse, parse_glob};
12
13#[derive(Debug)]
14#[non_exhaustive]
15pub struct Config {
16    pub database: Database,
17    pub database_prefix: String,
18    pub redis: RedisConfig,
19    pub notify_push_redis: Option<RedisConfig>,
20    pub nextcloud_url: String,
21}
22
23#[derive(Debug)]
24pub enum RedisConfig {
25    Single(RedisConnectionInfo),
26    Cluster(RedisClusterConnectionInfo),
27}
28
29impl RedisConfig {
30    pub fn as_single(&self) -> Option<RedisConnectionInfo> {
31        match self {
32            RedisConfig::Single(single) => Some(single.clone()),
33            RedisConfig::Cluster(cluster) => cluster.iter().next(),
34        }
35    }
36}
37
38#[derive(Clone, Debug, PartialOrd, PartialEq, Ord, Eq)]
39pub enum RedisConnectionAddr {
40    Tcp { host: String, port: u16, tls: bool },
41    Unix { path: PathBuf },
42}
43
44impl RedisConnectionAddr {
45    fn parse(mut host: &str, port: Option<u16>, tls: bool) -> Self {
46        if host.starts_with("/") {
47            RedisConnectionAddr::Unix { path: host.into() }
48        } else {
49            let tls = if host.starts_with("tls://") || host.starts_with("rediss://") {
50                host = host.split_once("://").unwrap().1;
51                true
52            } else {
53                tls
54            };
55            if host == "localhost" {
56                host = "127.0.0.1";
57            }
58            let (host, port, _) = if let Some(port) = port {
59                (host, Some(port), None)
60            } else {
61                split_host(host)
62            };
63            RedisConnectionAddr::Tcp {
64                host: host.into(),
65                port: port.unwrap_or(6379),
66                tls,
67            }
68        }
69    }
70}
71
72#[derive(Clone, Debug)]
73pub struct RedisClusterConnectionInfo {
74    pub addr: Vec<RedisConnectionAddr>,
75    pub db: i64,
76    pub username: Option<String>,
77    pub password: Option<String>,
78    pub tls_params: Option<RedisTlsParams>,
79}
80
81impl RedisClusterConnectionInfo {
82    pub fn iter(&self) -> impl Iterator<Item = RedisConnectionInfo> + '_ {
83        self.addr.iter().cloned().map(|addr| RedisConnectionInfo {
84            addr,
85            db: self.db,
86            username: self.username.clone(),
87            password: self.password.clone(),
88            tls_params: self.tls_params.clone(),
89        })
90    }
91}
92
93#[derive(Clone, Debug)]
94pub struct RedisConnectionInfo {
95    pub addr: RedisConnectionAddr,
96    pub db: i64,
97    pub username: Option<String>,
98    pub password: Option<String>,
99    pub tls_params: Option<RedisTlsParams>,
100}
101
102#[derive(Clone, Debug, Default)]
103pub struct RedisTlsParams {
104    pub local_cert: Option<PathBuf>,
105    pub local_pk: Option<PathBuf>,
106    pub ca_file: Option<PathBuf>,
107    pub accept_invalid_hostname: bool,
108    pub insecure: bool,
109}
110
111impl RedisConfig {
112    pub fn addr(&self) -> impl Iterator<Item = &RedisConnectionAddr> {
113        match self {
114            RedisConfig::Single(conn) => Either::Left(once(&conn.addr)),
115            RedisConfig::Cluster(cluster) => Either::Right(cluster.addr.iter()),
116        }
117    }
118
119    pub fn db(&self) -> i64 {
120        match self {
121            RedisConfig::Single(conn) => conn.db,
122            RedisConfig::Cluster(cluster) => cluster.db,
123        }
124    }
125
126    pub fn username(&self) -> Option<&str> {
127        match self {
128            RedisConfig::Single(conn) => conn.username.as_deref(),
129            RedisConfig::Cluster(cluster) => cluster.username.as_deref(),
130        }
131    }
132
133    pub fn passwd(&self) -> Option<&str> {
134        match self {
135            RedisConfig::Single(conn) => conn.password.as_deref(),
136            RedisConfig::Cluster(cluster) => cluster.password.as_deref(),
137        }
138    }
139
140    pub fn is_empty(&self) -> bool {
141        match self {
142            RedisConfig::Single(_) => false,
143            RedisConfig::Cluster(cluster) => cluster.addr.is_empty(),
144        }
145    }
146}
147
148type Result<T, E = Error> = std::result::Result<T, E>;
149
150#[derive(Debug, Error, Diagnostic)]
151pub enum Error {
152    #[error(transparent)]
153    #[diagnostic(transparent)]
154    Php(PhpParseError),
155    #[error("Provided config file doesn't seem to be a nextcloud config file: {0:#}")]
156    NotAConfig(#[from] NotAConfigError),
157    #[error("Failed to read config file")]
158    ReadFailed(std::io::Error, PathBuf),
159    #[error("invalid database configuration: {0}")]
160    InvalidDb(#[from] DbError),
161    #[error("Invalid redis configuration")]
162    Redis,
163    #[error("`overwrite.cli.url` not set`")]
164    NoUrl,
165}
166
167#[derive(Debug, Error, Diagnostic)]
168#[error("Error while parsing '{path}':\n{err}")]
169#[diagnostic(forward(err))]
170pub struct PhpParseError {
171    err: php_literal_parser::ParseError,
172    path: PathBuf,
173}
174
175#[derive(Debug, Error)]
176pub enum DbError {
177    #[error("unsupported database type {0}")]
178    Unsupported(String),
179    #[error("no username set")]
180    NoUsername,
181    #[error("no password set")]
182    NoPassword,
183    #[error("no data directory")]
184    NoDataDirectory,
185}
186
187#[derive(Debug, Error)]
188pub enum NotAConfigError {
189    #[error("$CONFIG not found in file")]
190    NoConfig(PathBuf),
191    #[error("$CONFIG is not an array")]
192    NotAnArray(PathBuf),
193}
194
195#[derive(Debug, Clone)]
196pub enum SslOptions {
197    Enabled {
198        key: String,
199        cert: String,
200        ca: String,
201        verify: bool,
202    },
203    Disabled,
204    Default,
205}
206
207#[derive(Debug, Clone)]
208pub enum Database {
209    Sqlite {
210        database: PathBuf,
211    },
212    MySql {
213        database: String,
214        username: String,
215        password: String,
216        connect: DbConnect,
217        ssl_options: SslOptions,
218    },
219    Postgres {
220        database: String,
221        username: String,
222        password: String,
223        connect: DbConnect,
224        ssl_options: SslOptions,
225    },
226}
227
228#[derive(Debug, Clone)]
229pub enum DbConnect {
230    Tcp { host: String, port: u16 },
231    Socket(PathBuf),
232}
233
234impl Database {
235    pub fn url(&self) -> String {
236        match self {
237            Database::Sqlite { database } => {
238                format!("sqlite://{}", database.display())
239            }
240            Database::MySql {
241                database,
242                username,
243                password,
244                connect,
245                ssl_options,
246            } => {
247                let mut params = Serializer::new(String::new());
248                match ssl_options {
249                    SslOptions::Default => {}
250                    SslOptions::Disabled => {
251                        params.append_pair("ssl-mode", "disabled");
252                    }
253                    SslOptions::Enabled { ca, verify, .. } => {
254                        params.append_pair(
255                            "ssl-mode",
256                            if *verify {
257                                "verify_identity"
258                            } else {
259                                "verify_ca"
260                            },
261                        );
262                        params.append_pair("ssl-ca", ca.as_str());
263                    }
264                }
265                let (host, port) = match connect {
266                    DbConnect::Socket(socket) => {
267                        params.append_pair("socket", &socket.to_string_lossy());
268                        ("localhost", 3306) // ignored when socket is set
269                    }
270                    DbConnect::Tcp { host, port } => (host.as_str(), *port),
271                };
272                let params = params.finish().replace("%2F", "/");
273                let params_start = if params.is_empty() { "" } else { "?" };
274
275                if port == 3306 {
276                    format!(
277                        "mysql://{}:{}@{}/{}{}{}",
278                        urlencoding::encode(username),
279                        urlencoding::encode(password),
280                        host,
281                        database,
282                        params_start,
283                        params
284                    )
285                } else {
286                    format!(
287                        "mysql://{}:{}@{}:{}/{}{}{}",
288                        urlencoding::encode(username),
289                        urlencoding::encode(password),
290                        host,
291                        port,
292                        database,
293                        params_start,
294                        params
295                    )
296                }
297            }
298            Database::Postgres {
299                database,
300                username,
301                password,
302                connect,
303                ssl_options,
304            } => {
305                let mut params = Serializer::new(String::new());
306                match ssl_options {
307                    SslOptions::Default => {}
308                    SslOptions::Disabled => {
309                        params.append_pair("sslmode", "disable");
310                    }
311                    SslOptions::Enabled { ca, verify, .. } => {
312                        params.append_pair(
313                            "ssl-mode",
314                            if *verify { "verify-full" } else { "verify-ca" },
315                        );
316                        params.append_pair("sslrootcert", ca.as_str());
317                    }
318                }
319                let (host, port) = match connect {
320                    DbConnect::Socket(socket) => {
321                        params.append_pair("host", &socket.to_string_lossy());
322                        ("localhost", 5432) // ignored when socket is set
323                    }
324                    DbConnect::Tcp { host, port } => (host.as_str(), *port),
325                };
326                let params = params.finish().replace("%2F", "/");
327                let params_start = if params.is_empty() { "" } else { "?" };
328
329                if port == 5432 {
330                    format!(
331                        "postgresql://{}:{}@{}/{}{}{}",
332                        urlencoding::encode(username),
333                        urlencoding::encode(password),
334                        host,
335                        database,
336                        params_start,
337                        params
338                    )
339                } else {
340                    format!(
341                        "postgresql://{}:{}@{}:{}/{}{}{}",
342                        urlencoding::encode(username),
343                        urlencoding::encode(password),
344                        host,
345                        port,
346                        database,
347                        params_start,
348                        params
349                    )
350                }
351            }
352        }
353    }
354}
355
356fn split_host(host: &str) -> (&str, Option<u16>, Option<&str>) {
357    if host.starts_with('/') {
358        return ("localhost", None, Some(host));
359    }
360    let mut parts = host.split(':');
361    let host = parts.next().unwrap();
362    match parts
363        .next()
364        .map(|port_or_socket| u16::from_str(port_or_socket).map_err(|_| port_or_socket))
365    {
366        Some(Ok(port)) => (host, Some(port), None),
367        Some(Err(socket)) => (host, None, Some(socket)),
368        None => (host, None, None),
369    }
370}