Skip to main content

nextcloud_config_parser/
lib.rs

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