1use base64::Engine;
8use faucet_core::FaucetError;
9use serde_json::Value;
10
11const OID_BOOL: u32 = 16;
13const OID_BYTEA: u32 = 17;
14const OID_INT2: u32 = 21;
15const OID_INT4: u32 = 23;
16const OID_INT8: u32 = 20;
17const OID_FLOAT4: u32 = 700;
18const OID_FLOAT8: u32 = 701;
19const OID_NUMERIC: u32 = 1700;
20const OID_JSON: u32 = 114;
21const OID_JSONB: u32 = 3802;
22fn array_element_oid(array_oid: u32) -> Option<u32> {
33 Some(match array_oid {
34 1000 => OID_BOOL, 1001 => OID_BYTEA, 1005 => OID_INT2, 1007 => OID_INT4, 1016 => OID_INT8, 1021 => OID_FLOAT4, 1022 => OID_FLOAT8, 1231 => OID_NUMERIC, 199 => OID_JSON, 3807 => OID_JSONB, 1009 => 25, 1015 => 1043, 1014 => 1042, 2951 => 2950, 1115 => 1114, 1185 => 1184, 1182 => 1082, 1183 => 1083, _ => return None,
53 })
54}
55
56pub fn text_to_json(type_oid: u32, text: &str) -> Result<Value, FaucetError> {
64 Ok(match type_oid {
65 OID_BOOL => match text {
66 "t" => Value::Bool(true),
67 "f" => Value::Bool(false),
68 other => {
69 return Err(FaucetError::Source(format!(
70 "pgoutput: bool column has non-t/f text {other:?}"
71 )));
72 }
73 },
74 OID_INT2 | OID_INT4 | OID_INT8 => {
75 let n: i64 = text.parse().map_err(|e| {
76 FaucetError::Source(format!(
77 "pgoutput: int (oid={type_oid}) parse {text:?}: {e}"
78 ))
79 })?;
80 Value::from(n)
81 }
82 OID_FLOAT4 | OID_FLOAT8 => match text {
83 "NaN" => Value::String("NaN".into()),
87 "Infinity" => Value::String("Infinity".into()),
88 "-Infinity" => Value::String("-Infinity".into()),
89 other => {
90 let n: f64 = other.parse().map_err(|e| {
91 FaucetError::Source(format!("pgoutput: float parse {text:?}: {e}"))
92 })?;
93 serde_json::Number::from_f64(n)
94 .map(Value::Number)
95 .unwrap_or(Value::Null)
96 }
97 },
98 OID_NUMERIC => Value::String(text.into()),
99 OID_BYTEA => {
100 let stripped = text.strip_prefix("\\x").ok_or_else(|| {
101 FaucetError::Source(format!(
102 "pgoutput: bytea text {text:?} missing '\\x' prefix"
103 ))
104 })?;
105 let bytes = hex_decode(stripped)?;
106 Value::String(base64::engine::general_purpose::STANDARD.encode(bytes))
107 }
108 OID_JSON | OID_JSONB => serde_json::from_str(text).map_err(|e| {
109 FaucetError::Source(format!("pgoutput: json/jsonb parse {text:?}: {e}"))
110 })?,
111 other => {
112 if let Some(elem_oid) = array_element_oid(other)
117 && let Some(elements) = parse_pg_array(text)
118 {
119 let mut out = Vec::with_capacity(elements.len());
120 for elem in elements {
121 match elem {
122 Some(s) => out.push(text_to_json(elem_oid, &s)?),
123 None => out.push(Value::Null),
124 }
125 }
126 Value::Array(out)
127 } else {
128 Value::String(text.into())
129 }
130 }
131 })
132}
133
134fn parse_pg_array(text: &str) -> Option<Vec<Option<String>>> {
140 let bytes = text.as_bytes();
141 if bytes.first() != Some(&b'{') || bytes.last() != Some(&b'}') {
142 return None;
143 }
144 let inner = &text[1..text.len() - 1];
145 if inner.is_empty() {
146 return Some(Vec::new());
147 }
148
149 let mut out: Vec<Option<String>> = Vec::new();
150 let mut cur = String::new();
151 let mut in_quotes = false;
152 let mut quoted = false; let mut chars = inner.chars().peekable();
154
155 while let Some(c) = chars.next() {
156 if in_quotes {
157 match c {
158 '\\' => {
159 if let Some(next) = chars.next() {
161 cur.push(next);
162 }
163 }
164 '"' => in_quotes = false,
165 _ => cur.push(c),
166 }
167 continue;
168 }
169 match c {
170 '"' => {
171 in_quotes = true;
172 quoted = true;
173 }
174 '{' => return None,
176 ',' => {
177 out.push(finish_array_element(&cur, quoted));
178 cur.clear();
179 quoted = false;
180 }
181 _ => cur.push(c),
182 }
183 }
184 if in_quotes {
185 return None; }
187 out.push(finish_array_element(&cur, quoted));
188 Some(out)
189}
190
191fn finish_array_element(raw: &str, quoted: bool) -> Option<String> {
194 if !quoted && raw == "NULL" {
195 None
196 } else {
197 Some(raw.to_owned())
198 }
199}
200
201fn hex_decode(s: &str) -> Result<Vec<u8>, FaucetError> {
202 if !s.len().is_multiple_of(2) {
203 return Err(FaucetError::Source(format!(
204 "pgoutput: bytea hex has odd length: {s:?}"
205 )));
206 }
207 let mut out = Vec::with_capacity(s.len() / 2);
208 for i in (0..s.len()).step_by(2) {
209 out.push(
210 u8::from_str_radix(&s[i..i + 2], 16)
211 .map_err(|e| FaucetError::Source(format!("pgoutput: bytea hex {s:?}: {e}")))?,
212 );
213 }
214 Ok(out)
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use serde_json::json;
221
222 #[test]
223 fn bool_t_and_f() {
224 assert_eq!(text_to_json(OID_BOOL, "t").unwrap(), json!(true));
225 assert_eq!(text_to_json(OID_BOOL, "f").unwrap(), json!(false));
226 assert!(text_to_json(OID_BOOL, "yes").is_err());
227 }
228
229 #[test]
230 fn integer_types() {
231 assert_eq!(text_to_json(OID_INT2, "32000").unwrap(), json!(32000));
232 assert_eq!(text_to_json(OID_INT4, "-1").unwrap(), json!(-1));
233 assert_eq!(
234 text_to_json(OID_INT8, "9223372036854775807").unwrap(),
235 json!(9223372036854775807_i64)
236 );
237 assert!(text_to_json(OID_INT4, "abc").is_err());
238 }
239
240 #[test]
241 fn floats() {
242 assert_eq!(text_to_json(OID_FLOAT8, "3.5").unwrap(), json!(3.5));
243 assert_eq!(text_to_json(OID_FLOAT8, "NaN").unwrap(), json!("NaN"));
245 assert_eq!(
246 text_to_json(OID_FLOAT4, "Infinity").unwrap(),
247 json!("Infinity")
248 );
249 assert_eq!(
250 text_to_json(OID_FLOAT8, "-Infinity").unwrap(),
251 json!("-Infinity")
252 );
253 }
254
255 #[test]
256 fn int_array_decodes_to_json_array() {
257 assert_eq!(text_to_json(1007, "{1,2,3}").unwrap(), json!([1, 2, 3]));
258 assert_eq!(text_to_json(1007, "{}").unwrap(), json!([]));
259 assert_eq!(text_to_json(1016, "{-9,0,9}").unwrap(), json!([-9, 0, 9]));
260 }
261
262 #[test]
263 fn text_array_handles_quotes_nulls_and_commas() {
264 assert_eq!(
265 text_to_json(1009, r#"{a,"b,c",NULL,"NULL"}"#).unwrap(),
266 json!(["a", "b,c", null, "NULL"])
267 );
268 assert_eq!(
270 text_to_json(1009, r#"{"he said \"hi\""}"#).unwrap(),
271 json!(["he said \"hi\""])
272 );
273 }
274
275 #[test]
276 fn bool_array_decodes() {
277 assert_eq!(
278 text_to_json(1000, "{t,f,t}").unwrap(),
279 json!([true, false, true])
280 );
281 }
282
283 #[test]
284 fn multidimensional_array_falls_back_to_string() {
285 assert_eq!(
286 text_to_json(1007, "{{1,2},{3,4}}").unwrap(),
287 json!("{{1,2},{3,4}}")
288 );
289 }
290
291 #[test]
292 fn numeric_kept_as_string() {
293 assert_eq!(
294 text_to_json(OID_NUMERIC, "12345.67890").unwrap(),
295 json!("12345.67890")
296 );
297 }
298
299 #[test]
300 fn bytea_base64() {
301 assert_eq!(
303 text_to_json(OID_BYTEA, "\\xDEADBEEF").unwrap(),
304 json!("3q2+7w==")
305 );
306 assert!(text_to_json(OID_BYTEA, "deadbeef").is_err()); assert!(text_to_json(OID_BYTEA, "\\xZZ").is_err()); }
309
310 #[test]
311 fn json_columns_parsed() {
312 assert_eq!(
313 text_to_json(OID_JSON, r#"{"a":1}"#).unwrap(),
314 json!({"a": 1})
315 );
316 assert_eq!(
317 text_to_json(OID_JSONB, r#"[1,2,3]"#).unwrap(),
318 json!([1, 2, 3])
319 );
320 }
321
322 #[test]
323 fn unknown_oid_falls_back_to_string() {
324 assert_eq!(
325 text_to_json(99999, "2026-05-17 12:34:56+00").unwrap(),
326 json!("2026-05-17 12:34:56+00")
327 );
328 }
329
330 #[test]
331 fn int2_and_int8_arrays_decode() {
332 assert_eq!(text_to_json(1005, "{1,2,3}").unwrap(), json!([1, 2, 3]));
334 assert_eq!(text_to_json(1005, "{-7,7}").unwrap(), json!([-7, 7]));
336 }
337
338 #[test]
339 fn float_arrays_decode() {
340 assert_eq!(text_to_json(1021, "{1.5,2.5}").unwrap(), json!([1.5, 2.5]));
342 assert_eq!(
343 text_to_json(1022, "{3.25,-4.75}").unwrap(),
344 json!([3.25, -4.75])
345 );
346 }
347
348 #[test]
349 fn numeric_array_keeps_elements_as_strings() {
350 assert_eq!(
352 text_to_json(1231, "{1.10,2.20}").unwrap(),
353 json!(["1.10", "2.20"])
354 );
355 }
356
357 #[test]
358 fn bytea_array_decodes_to_base64_elements() {
359 assert_eq!(
361 text_to_json(1001, "{\\xDEADBEEF}").unwrap(),
362 json!(["3q2+7w=="])
363 );
364 }
365
366 #[test]
367 fn json_and_jsonb_arrays_decode_elements() {
368 assert_eq!(
372 text_to_json(199, r#"{"{\"a\":1}"}"#).unwrap(),
373 json!([{"a": 1}])
374 );
375 assert_eq!(text_to_json(3807, r#"{"[1,2]"}"#).unwrap(), json!([[1, 2]]));
376 }
377
378 #[test]
379 fn varchar_bpchar_uuid_arrays_decode_to_strings() {
380 assert_eq!(text_to_json(1015, "{a,b}").unwrap(), json!(["a", "b"]));
383 assert_eq!(text_to_json(1014, "{x,y}").unwrap(), json!(["x", "y"]));
384 assert_eq!(
385 text_to_json(2951, "{11111111-1111-1111-1111-111111111111}").unwrap(),
386 json!(["11111111-1111-1111-1111-111111111111"])
387 );
388 }
389
390 #[test]
391 fn datetime_arrays_decode_to_strings() {
392 assert_eq!(
396 text_to_json(1115, r#"{"2026-05-17 12:34:56"}"#).unwrap(),
397 json!(["2026-05-17 12:34:56"])
398 );
399 assert_eq!(
400 text_to_json(1185, r#"{"2026-05-17 12:34:56+00"}"#).unwrap(),
401 json!(["2026-05-17 12:34:56+00"])
402 );
403 assert_eq!(
404 text_to_json(1182, "{2026-05-17}").unwrap(),
405 json!(["2026-05-17"])
406 );
407 assert_eq!(
408 text_to_json(1183, "{12:34:56}").unwrap(),
409 json!(["12:34:56"])
410 );
411 }
412
413 #[test]
414 fn float_array_element_parse_error_propagates() {
415 let err = text_to_json(1022, "{not_a_float}").unwrap_err();
418 assert!(err.to_string().contains("float parse"), "{err}");
419 }
420
421 #[test]
422 fn json_array_element_parse_error_propagates() {
423 let err = text_to_json(199, "{notjson}").unwrap_err();
426 assert!(err.to_string().contains("json/jsonb parse"), "{err}");
427 }
428
429 #[test]
430 fn array_oid_with_non_brace_text_falls_back_to_string() {
431 assert_eq!(
435 text_to_json(1007, "not-an-array").unwrap(),
436 json!("not-an-array")
437 );
438 }
439
440 #[test]
441 fn array_with_unterminated_quote_falls_back_to_string() {
442 let raw = r#"{"unterminated}"#;
445 assert_eq!(text_to_json(1009, raw).unwrap(), json!(raw));
446 }
447
448 #[test]
449 fn bytea_odd_length_hex_errors() {
450 let err = text_to_json(OID_BYTEA, "\\xABC").unwrap_err();
452 assert!(err.to_string().contains("odd length"), "{err}");
453 }
454}