use alloc::string::String;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoFileCall {
Import { path: String, oid: Option<u32> },
Export { oid: u32, path: String },
}
impl LoFileCall {
#[must_use]
pub const fn column_name(&self) -> &'static str {
match self {
Self::Import { .. } => "lo_import",
Self::Export { .. } => "lo_export",
}
}
}
#[must_use]
pub fn parse_lo_file_call(sql: &str) -> Option<LoFileCall> {
let t = sql.trim().trim_end_matches(';').trim();
let rest = strip_prefix_ci(t, "select")?.trim_start();
let (name, args) = split_call(rest)?;
let args = split_args(args);
match name.as_str() {
"lo_import" => match args.as_slice() {
[p] => Some(LoFileCall::Import {
path: string_literal(p)?,
oid: None,
}),
[p, o] => Some(LoFileCall::Import {
path: string_literal(p)?,
oid: Some(o.trim().parse().ok()?),
}),
_ => None,
},
"lo_export" => match args.as_slice() {
[o, p] => Some(LoFileCall::Export {
oid: o.trim().parse().ok()?,
path: string_literal(p)?,
}),
_ => None,
},
_ => None,
}
}
#[must_use]
pub fn could_not_open(path: &str, os_error: &str) -> String {
alloc::format!(
"could not open server file \"{path}\": {}",
trim_os(os_error)
)
}
#[must_use]
pub fn could_not_create(path: &str, os_error: &str) -> String {
alloc::format!(
"could not create server file \"{path}\": {}",
trim_os(os_error)
)
}
#[must_use]
pub fn permission_denied(call: &LoFileCall) -> String {
alloc::format!("permission denied for function {}", call.column_name())
}
fn trim_os(os_error: &str) -> &str {
os_error.split(" (os error").next().unwrap_or(os_error)
}
fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
if s.len() >= prefix.len() && s[..prefix.len()].eq_ignore_ascii_case(prefix) {
Some(&s[prefix.len()..])
} else {
None
}
}
fn split_call(s: &str) -> Option<(String, &str)> {
let open = s.find('(')?;
let close = s.rfind(')')?;
if close < open {
return None;
}
let name = s[..open].trim().to_ascii_lowercase();
if !s[close + 1..].trim().is_empty() {
return None;
}
Some((name, &s[open + 1..close]))
}
fn split_args(s: &str) -> alloc::vec::Vec<&str> {
let mut out = alloc::vec::Vec::new();
let mut start = 0usize;
let mut in_quote = false;
for (i, c) in s.char_indices() {
match c {
'\'' => in_quote = !in_quote,
',' if !in_quote => {
out.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
if !s[start..].trim().is_empty() || !out.is_empty() {
out.push(&s[start..]);
}
out
}
fn string_literal(s: &str) -> Option<String> {
let t = s.trim();
let inner = t.strip_prefix('\'')?.strip_suffix('\'')?;
Some(inner.replace("''", "'"))
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn recognises_both_calls() {
assert_eq!(
parse_lo_file_call("SELECT lo_import('/tmp/a.txt')"),
Some(LoFileCall::Import {
path: "/tmp/a.txt".to_string(),
oid: None
})
);
assert_eq!(
parse_lo_file_call("select LO_IMPORT('/tmp/a.txt', 4242);"),
Some(LoFileCall::Import {
path: "/tmp/a.txt".to_string(),
oid: Some(4242)
})
);
assert_eq!(
parse_lo_file_call("SELECT lo_export(4242, '/tmp/b.bin')"),
Some(LoFileCall::Export {
oid: 4242,
path: "/tmp/b.bin".to_string()
})
);
}
#[test]
fn leaves_everything_else_alone() {
for sql in [
"SELECT lo_get(1)",
"SELECT 1",
"SELECT lo_import('/tmp/a') FROM t",
"SELECT length(lo_import('/tmp/a'))",
"INSERT INTO t VALUES (lo_import('/tmp/a'))",
] {
assert_eq!(parse_lo_file_call(sql), None, "for `{sql}`");
}
}
#[test]
fn a_path_may_hold_a_comma_or_a_quote() {
assert_eq!(
parse_lo_file_call("SELECT lo_import('/tmp/a,b.txt')"),
Some(LoFileCall::Import {
path: "/tmp/a,b.txt".to_string(),
oid: None
})
);
assert_eq!(
parse_lo_file_call("SELECT lo_import('/tmp/it''s.txt')"),
Some(LoFileCall::Import {
path: "/tmp/it's.txt".to_string(),
oid: None
})
);
}
}