fallback to having php parse the config file and exporting it as json

This commit is contained in:
Robin Appelman 2023-01-03 19:28:46 +01:00
commit f9486f3e70
5 changed files with 139 additions and 15 deletions

View file

@ -15,6 +15,8 @@ thiserror = "1.0.38"
php-literal-parser = "0.5.0"
sqlx = { version = "0.6.2", default-features = false, features = ["runtime-tokio-rustls", "any", "mysql", "sqlite", "postgres"], optional = true }
miette = "5.5.0"
serde_json = "1.0.87"
tracing = "0.1.37"
[dev-dependencies]
sqlx = { version = "0.6.2", default-features = false, features = ["runtime-tokio-rustls", "any", "mysql", "sqlite", "postgres"] }

View file

@ -1,8 +1,10 @@
mod nc;
mod php;
use miette::Diagnostic;
#[cfg(feature = "redis-connect")]
use redis::{ConnectionAddr, ConnectionInfo};
use std::fmt::Debug;
#[cfg(feature = "redis-connect")]
use std::iter::once;
use std::path::PathBuf;
@ -90,6 +92,8 @@ pub enum Error {
Redis,
#[error("`overwrite.cli.url` not set`")]
NoUrl,
#[error("Failed to execute php to parse configuration")]
Exec,
}
#[derive(Debug, Error, Diagnostic)]
@ -234,3 +238,9 @@ impl From<Database> for sqlx::any::AnyConnectOptions {
}
}
}
#[cfg(test)]
#[track_caller]
pub fn assert_debug_equal<T: Debug>(a: T, b: T) {
assert_eq!(format!("{:?}", a), format!("{:?}", b),);
}

View file

@ -65,12 +65,20 @@ fn parse_php(path: impl AsRef<Path>) -> Result<Value> {
)));
}
};
php_literal_parser::from_str(php).map_err(|err| {
Error::Php(PhpParseError {
err,
path: path.as_ref().into(),
})
})
match php_literal_parser::from_str(php) {
Ok(config) => Ok(config),
Err(err) => {
// attempt parsing the config file with php
if let Ok(config) = try_exec_config(&path) {
Ok(config)
} else {
Err(Error::Php(PhpParseError {
err,
path: path.as_ref().into(),
}))
}
}
}
}
fn merge_configs(input: Vec<(PathBuf, Value)>) -> Result<Value> {
@ -131,7 +139,7 @@ pub fn parse_glob(path: impl AsRef<Path>) -> Result<Config> {
parse_files(glob_config_files(path))
}
fn parse_db_options(parsed: &Value) -> Result<Database> {
pub(crate) fn parse_db_options(parsed: &Value) -> Result<Database> {
match parsed["dbtype"].as_str() {
Some("mysql") => {
let username = parsed["dbuser"].as_str().ok_or(DbError::NoUsername)?;
@ -384,16 +392,11 @@ fn test_redis_empty_password_none() {
}
#[cfg(test)]
#[track_caller]
fn assert_debug_equal<T: Debug>(a: T, b: T) {
assert_eq!(format!("{:?}", a), format!("{:?}", b),);
}
use crate::assert_debug_equal;
use crate::php::try_exec_config;
#[cfg(test)]
#[allow(unused_imports)]
use sqlx::{any::AnyConnectOptions, postgres::PgConnectOptions};
#[cfg(test)]
use std::fmt::Debug;
#[cfg(test)]
fn config_from_file(path: &str) -> Config {

92
src/php.rs Normal file
View file

@ -0,0 +1,92 @@
use crate::Error;
use php_literal_parser::{Key, Value};
use std::path::Path;
use std::process::Command;
use tracing::{debug, warn};
fn from_json(json: serde_json::Value) -> Value {
match json {
serde_json::Value::Null => Value::Null,
serde_json::Value::Bool(b) => Value::Bool(b),
serde_json::Value::Number(n) => {
if let Some(f) = n.as_f64() {
Value::Float(f)
} else if let Some(i) = n.as_i64() {
Value::Int(i)
} else {
// > i64::MAX
Value::Null
}
}
serde_json::Value::String(s) => Value::String(s),
serde_json::Value::Array(a) => Value::Array(
a.into_iter()
.enumerate()
.map(|(i, v)| (Key::Int(i as i64), from_json(v)))
.collect(),
),
serde_json::Value::Object(o) => Value::Array(
o.into_iter()
.map(|(i, v)| (Key::String(i), from_json(v)))
.collect(),
),
}
}
#[tracing::instrument(skip_all, fields(path = %path.as_ref().display()))]
pub fn try_exec_config<P: AsRef<Path>>(path: P) -> Result<Value, Error> {
debug!("Attempting executing the config file php as fallback");
let path = path.as_ref();
let cmd = Command::new("php")
.arg("-r")
.arg(format!(
r#"
include "{}";
echo json_encode($CONFIG);
"#,
path.display()
))
.output()
.map_err(|e| {
warn!(
config_file = %path.display(),
error = %e,
"error while executing config file with php"
);
Error::Exec
})?;
let stdout = cmd.stdout;
let json: serde_json::Value = serde_json::from_slice(&stdout).map_err(|_e| {
warn!(
config_file = %path.display(),
json = ?std::str::from_utf8(&stdout),
"php returned invalid json"
);
Error::Exec
})?;
Ok(from_json(json))
}
#[cfg(test)]
use crate::nc::parse_db_options;
#[cfg(test)]
use crate::{assert_debug_equal, Database, DbConnect, SslOptions};
#[test]
fn test_parse_redis_socket() {
let database =
parse_db_options(&try_exec_config("tests/configs/non_literal.php").unwrap()).unwrap();
assert_debug_equal(
&Database::MySql {
database: "foo_db".to_string(),
username: "nextcloud".to_string(),
password: "secret".to_string(),
connect: DbConnect::Tcp {
host: "127.0.0.1".to_string(),
port: 3306,
},
ssl_options: SslOptions::Disabled,
},
&database,
);
}

View file

@ -0,0 +1,17 @@
<?php
function get_foo(): string { return 'foo'; }
$CONFIG = [
'overwrite.cli.url' => 'https://cloud.example.com',
'dbtype' => 'mysql',
'dbname' => get_foo() . '_db',
'dbhost' => '127.0.0.1',
'dbport' => '',
'dbtableprefix' => 'oc_',
'dbuser' => 'nextcloud',
'dbpassword' => 'secret',
'redis' => [
'host' => 'localhost'
]
];