spg_engine/
largeobject.rs1use alloc::string::String;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum LoFileCall {
30 Import { path: String, oid: Option<u32> },
32 Export { oid: u32, path: String },
34}
35
36impl LoFileCall {
37 #[must_use]
39 pub const fn column_name(&self) -> &'static str {
40 match self {
41 Self::Import { .. } => "lo_import",
42 Self::Export { .. } => "lo_export",
43 }
44 }
45}
46
47#[must_use]
54pub fn parse_lo_file_call(sql: &str) -> Option<LoFileCall> {
55 let t = sql.trim().trim_end_matches(';').trim();
56 let rest = strip_prefix_ci(t, "select")?.trim_start();
57 let (name, args) = split_call(rest)?;
58 let args = split_args(args);
59 match name.as_str() {
60 "lo_import" => match args.as_slice() {
61 [p] => Some(LoFileCall::Import {
62 path: string_literal(p)?,
63 oid: None,
64 }),
65 [p, o] => Some(LoFileCall::Import {
66 path: string_literal(p)?,
67 oid: Some(o.trim().parse().ok()?),
68 }),
69 _ => None,
70 },
71 "lo_export" => match args.as_slice() {
72 [o, p] => Some(LoFileCall::Export {
73 oid: o.trim().parse().ok()?,
74 path: string_literal(p)?,
75 }),
76 _ => None,
77 },
78 _ => None,
79 }
80}
81
82#[must_use]
84pub fn could_not_open(path: &str, os_error: &str) -> String {
85 alloc::format!(
86 "could not open server file \"{path}\": {}",
87 trim_os(os_error)
88 )
89}
90
91#[must_use]
93pub fn could_not_create(path: &str, os_error: &str) -> String {
94 alloc::format!(
95 "could not create server file \"{path}\": {}",
96 trim_os(os_error)
97 )
98}
99
100#[must_use]
102pub fn permission_denied(call: &LoFileCall) -> String {
103 alloc::format!("permission denied for function {}", call.column_name())
104}
105
106fn trim_os(os_error: &str) -> &str {
109 os_error.split(" (os error").next().unwrap_or(os_error)
110}
111
112fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
113 if s.len() >= prefix.len() && s[..prefix.len()].eq_ignore_ascii_case(prefix) {
114 Some(&s[prefix.len()..])
115 } else {
116 None
117 }
118}
119
120fn split_call(s: &str) -> Option<(String, &str)> {
122 let open = s.find('(')?;
123 let close = s.rfind(')')?;
124 if close < open {
125 return None;
126 }
127 let name = s[..open].trim().to_ascii_lowercase();
128 if !s[close + 1..].trim().is_empty() {
129 return None;
130 }
131 Some((name, &s[open + 1..close]))
132}
133
134fn split_args(s: &str) -> alloc::vec::Vec<&str> {
136 let mut out = alloc::vec::Vec::new();
137 let mut start = 0usize;
138 let mut in_quote = false;
139 for (i, c) in s.char_indices() {
140 match c {
141 '\'' => in_quote = !in_quote,
142 ',' if !in_quote => {
143 out.push(&s[start..i]);
144 start = i + 1;
145 }
146 _ => {}
147 }
148 }
149 if !s[start..].trim().is_empty() || !out.is_empty() {
150 out.push(&s[start..]);
151 }
152 out
153}
154
155fn string_literal(s: &str) -> Option<String> {
157 let t = s.trim();
158 let inner = t.strip_prefix('\'')?.strip_suffix('\'')?;
159 Some(inner.replace("''", "'"))
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use alloc::string::ToString;
166
167 #[test]
168 fn recognises_both_calls() {
169 assert_eq!(
170 parse_lo_file_call("SELECT lo_import('/tmp/a.txt')"),
171 Some(LoFileCall::Import {
172 path: "/tmp/a.txt".to_string(),
173 oid: None
174 })
175 );
176 assert_eq!(
177 parse_lo_file_call("select LO_IMPORT('/tmp/a.txt', 4242);"),
178 Some(LoFileCall::Import {
179 path: "/tmp/a.txt".to_string(),
180 oid: Some(4242)
181 })
182 );
183 assert_eq!(
184 parse_lo_file_call("SELECT lo_export(4242, '/tmp/b.bin')"),
185 Some(LoFileCall::Export {
186 oid: 4242,
187 path: "/tmp/b.bin".to_string()
188 })
189 );
190 }
191
192 #[test]
193 fn leaves_everything_else_alone() {
194 for sql in [
195 "SELECT lo_get(1)",
196 "SELECT 1",
197 "SELECT lo_import('/tmp/a') FROM t",
198 "SELECT length(lo_import('/tmp/a'))",
199 "INSERT INTO t VALUES (lo_import('/tmp/a'))",
200 ] {
201 assert_eq!(parse_lo_file_call(sql), None, "for `{sql}`");
202 }
203 }
204
205 #[test]
206 fn a_path_may_hold_a_comma_or_a_quote() {
207 assert_eq!(
208 parse_lo_file_call("SELECT lo_import('/tmp/a,b.txt')"),
209 Some(LoFileCall::Import {
210 path: "/tmp/a,b.txt".to_string(),
211 oid: None
212 })
213 );
214 assert_eq!(
215 parse_lo_file_call("SELECT lo_import('/tmp/it''s.txt')"),
216 Some(LoFileCall::Import {
217 path: "/tmp/it's.txt".to_string(),
218 oid: None
219 })
220 );
221 }
222}