Skip to main content

rupg_func/
xml.rs

1//! The functions of `xml.c` that make and test `xml` values: `xmlcomment`, `xmltext`, `xml`, `text`, `xmlconcat2`, the tests of `xml_is_well_formed` and the SQL/XML functions with their own syntax, such as `XMLELEMENT`.
2
3use rupg_analyze::{XmlExpr, XmlOp};
4use rupg_common::{Error, Result, SqlState};
5use rupg_pgcatalog::builtin;
6use rupg_types::{
7    DateFormat, DateOrder, DateStyle, Value, XmlStandalone, date_out, escape_xml, oid,
8    timestamp_out, timestamptz_out, xml_base64, xml_binhex, xml_comment, xml_concat, xml_element,
9    xml_in, xml_pi, xml_root, xml_text,
10};
11
12use crate::io::{array_of, base_type, output};
13use crate::reg::text_arg;
14use crate::{Call, Kernel, Session, bad_value, type_error};
15
16/// The kernel of a function of `xml` by its `prosrc`.
17pub(crate) fn by_src(src: &str) -> Option<Kernel> {
18    Some(match src {
19        "xmlcomment" => xmlcomment,
20        "xmltext" => xmltext,
21        "texttoxml" => texttoxml,
22        "xmltotext" => xmltotext,
23        "xmlconcat2" => xmlconcat2,
24        "xmlvalidate" => xmlvalidate,
25        "xml_is_well_formed" => xml_is_well_formed,
26        "xml_is_well_formed_document" => xml_is_well_formed_document,
27        "xml_is_well_formed_content" => xml_is_well_formed_content,
28        _ => return None,
29    })
30}
31
32/// True when the `xmloption` of the session is `DOCUMENT`.
33fn document_option(session: &dyn Session) -> bool {
34    session.setting("xmloption").is_some_and(|v| v.eq_ignore_ascii_case("document"))
35}
36
37/// `xml_parse` with a hard error: the text checked as a document or as content. The warnings of libxml2 go to the client.
38fn parse(text: &str, document: bool, session: &dyn Session) -> Result<()> {
39    xml_in(text.as_bytes(), document, &mut |warning| {
40        session.warn(Error::new(SqlState::WARNING, warning));
41    })
42    .map_err(type_error)
43}
44
45/// `wellformed_xml`: true when the text parses as a document or as content.
46fn well_formed(text: &str, document: bool, session: &dyn Session) -> bool {
47    parse(text, document, session).is_ok()
48}
49
50fn xmlcomment(_: &Call<'_>, args: &[Value]) -> Result<Value> {
51    Ok(Value::text(xml_comment(text_arg(args)?).map_err(type_error)?))
52}
53
54fn xmltext(_: &Call<'_>, args: &[Value]) -> Result<Value> {
55    Ok(Value::text(xml_text(text_arg(args)?)))
56}
57
58/// `texttoxml`: `xml(text)`, which parses the text by the `xmloption` of the session.
59fn texttoxml(call: &Call<'_>, args: &[Value]) -> Result<Value> {
60    let text = text_arg(args)?;
61    parse(text, document_option(call.session), call.session)?;
62    Ok(Value::text(text))
63}
64
65/// `xmltotext`: `text(xml)`, the text as it is stored.
66fn xmltotext(_: &Call<'_>, args: &[Value]) -> Result<Value> {
67    Ok(Value::text(text_arg(args)?))
68}
69
70/// `xmlconcat2`, the transition function of `xmlagg`. It is not strict: a null argument does not count.
71fn xmlconcat2(_: &Call<'_>, args: &[Value]) -> Result<Value> {
72    match args {
73        [Value::Null, Value::Null] => Ok(Value::Null),
74        [Value::Null, v] | [v, Value::Null] => Ok(v.clone()),
75        [a, b] => {
76            let (a, b) = (a.as_str().ok_or_else(bad_value)?, b.as_str().ok_or_else(bad_value)?);
77            Ok(Value::text(xml_concat(&[a, b])))
78        }
79        _ => Err(bad_value()),
80    }
81}
82
83fn xmlvalidate(_: &Call<'_>, _: &[Value]) -> Result<Value> {
84    Err(Error::new(SqlState::FEATURE_NOT_SUPPORTED, "xmlvalidate is not implemented"))
85}
86
87fn xml_is_well_formed(call: &Call<'_>, args: &[Value]) -> Result<Value> {
88    let document = document_option(call.session);
89    Ok(Value::Bool(well_formed(text_arg(args)?, document, call.session)))
90}
91
92fn xml_is_well_formed_document(call: &Call<'_>, args: &[Value]) -> Result<Value> {
93    Ok(Value::Bool(well_formed(text_arg(args)?, true, call.session)))
94}
95
96fn xml_is_well_formed_content(call: &Call<'_>, args: &[Value]) -> Result<Value> {
97    Ok(Value::Bool(well_formed(text_arg(args)?, false, call.session)))
98}
99
100/// The error of an infinite date or timestamp, which XML does not have.
101fn infinite(what: &str) -> Error {
102    Error::new(SqlState::DATETIME_VALUE_OUT_OF_RANGE, format!("{what} out of range"))
103        .with_detail(format!("XML does not support infinite {what} values."))
104}
105
106/// `map_sql_value_to_xml_value`: the text of a value that is not null in XML. An array gives an `element` for each element that is not null. A `boolean`, a date, a timestamp and a `bytea` have the forms of XML Schema. The other types give their output, which has the special characters escaped when `escape` is true, except for `xml`.
107///
108/// # Errors
109///
110/// `22008` for an infinite date or timestamp, and the errors of the output of the type.
111pub fn xml_value(ty: u32, value: &Value, escape: bool, session: &dyn Session) -> Result<String> {
112    let ty = base_type(ty);
113    if let Value::Array(array) = value {
114        let element = array_of(ty)
115            .map(|(e, _)| e)
116            .or_else(|| builtin::type_by_oid(ty).map(|row| row.elem))
117            .unwrap_or(oid::UNKNOWN);
118        let mut out = String::new();
119        for v in array.values.iter().flatten() {
120            out.push_str("<element>");
121            out.push_str(&xml_value(element, v, true, session)?);
122            out.push_str("</element>");
123        }
124        return Ok(out);
125    }
126    let format = DateFormat { style: DateStyle::Xsd, order: DateOrder::Mdy };
127    let mut buf = Vec::new();
128    match (ty, value) {
129        (oid::BOOL, Value::Bool(b)) => return Ok(if *b { "true" } else { "false" }.to_string()),
130        (oid::DATE, Value::Date(v)) => {
131            date_out(*v, format, &mut buf);
132            if buf.ends_with(b"infinity") {
133                return Err(infinite("date"));
134            }
135        }
136        (oid::TIMESTAMP, Value::Timestamp(v)) => {
137            timestamp_out(*v, format, &mut buf).map_err(type_error)?;
138            if buf.ends_with(b"infinity") {
139                return Err(infinite("timestamp"));
140            }
141        }
142        (oid::TIMESTAMPTZ, Value::TimestampTz(v)) => {
143            timestamptz_out(*v, format, &*session.zone()?, &mut buf).map_err(type_error)?;
144            if buf.ends_with(b"infinity") {
145                return Err(infinite("timestamp"));
146            }
147        }
148        (oid::BYTEA, Value::Bytea(bytes)) => {
149            let hex = session.setting("xmlbinary").is_some_and(|v| v.eq_ignore_ascii_case("hex"));
150            return Ok(if hex { xml_binhex(bytes) } else { xml_base64(bytes) });
151        }
152        _ => {
153            output(ty, value, session, &mut buf)?;
154            let text = String::from_utf8(buf).map_err(|_| bad_value())?;
155            return Ok(if ty == oid::XML || !escape { text } else { escape_xml(&text) });
156        }
157    }
158    String::from_utf8(buf).map_err(|_| bad_value())
159}
160
161/// The text of an argument of type `text` or `xml`.
162fn text_of(value: &Value) -> Result<&str> {
163    value.as_str().ok_or_else(bad_value)
164}
165
166/// `ExecEvalXmlExpr`: the value of a SQL/XML function from the values of its named arguments and of its other arguments, in the order of [`XmlExpr::children`].
167///
168/// # Errors
169///
170/// The errors of the function, such as `2200M` for `XMLPARSE` of a value that is not a document.
171pub fn xml_expr(x: &XmlExpr, values: &[Value], session: &dyn Session) -> Result<Value> {
172    let (named, args) = values.split_at(x.named.len().min(values.len()));
173    let arg = |i: usize| args.get(i).ok_or_else(bad_value);
174    let name = x.name.as_deref().unwrap_or_default();
175    Ok(match x.op {
176        XmlOp::Concat => {
177            let texts: Vec<&str> =
178                args.iter().filter(|v| !v.is_null()).map(text_of).collect::<Result<_>>()?;
179            if texts.is_empty() { Value::Null } else { Value::text(xml_concat(&texts)) }
180        }
181        XmlOp::Element => {
182            let mut attributes = Vec::new();
183            for ((attr, e), v) in x.named.iter().zip(named) {
184                if !v.is_null() {
185                    attributes.push((attr.as_str(), xml_value(e.ty, v, false, session)?));
186                }
187            }
188            let mut content = Vec::new();
189            for (e, v) in x.args.iter().zip(args) {
190                if !v.is_null() {
191                    content.push(xml_value(e.ty, v, true, session)?);
192                }
193            }
194            let attributes: Vec<(&str, &str)> =
195                attributes.iter().map(|(n, v)| (*n, v.as_str())).collect();
196            Value::text(xml_element(name, &attributes, &content))
197        }
198        XmlOp::Forest => {
199            let mut out = String::new();
200            let mut any = false;
201            for ((tag, e), v) in x.named.iter().zip(named) {
202                if !v.is_null() {
203                    any = true;
204                    out.push_str(&format!("<{tag}>{}</{tag}>", xml_value(e.ty, v, true, session)?));
205                }
206            }
207            if any { Value::text(out) } else { Value::Null }
208        }
209        XmlOp::Parse => match arg(0)? {
210            Value::Null => Value::Null,
211            v => {
212                let text = text_of(v)?;
213                parse(text, x.document, session)?;
214                Value::text(text)
215            }
216        },
217        XmlOp::Pi => {
218            let (text, null) = match args.first() {
219                Some(Value::Null) => (None, true),
220                Some(v) => (Some(text_of(v)?), false),
221                None => (None, false),
222            };
223            xml_pi(name, text, null).map_err(type_error)?.map_or(Value::Null, Value::text)
224        }
225        XmlOp::Root => match arg(0)? {
226            Value::Null => Value::Null,
227            data => {
228                let version = match arg(1)? {
229                    Value::Null => None,
230                    v => Some(text_of(v)?),
231                };
232                let standalone = match arg(2)? {
233                    Value::Int4(0) => XmlStandalone::Yes,
234                    Value::Int4(1) => XmlStandalone::No,
235                    Value::Int4(2) => XmlStandalone::NoValue,
236                    _ => XmlStandalone::Omitted,
237                };
238                Value::text(xml_root(text_of(data)?, version, standalone))
239            }
240        },
241        XmlOp::Serialize => match arg(0)? {
242            Value::Null => Value::Null,
243            v => {
244                let text = text_of(v)?;
245                if x.indent {
246                    return Err(crate::not_yet("XMLSERIALIZE with INDENT"));
247                }
248                if x.document && xml_in(text.as_bytes(), true, &mut |_| {}).is_err() {
249                    return Err(Error::new(SqlState::NOT_AN_XML_DOCUMENT, "not an XML document"));
250                }
251                Value::text(text)
252            }
253        },
254        XmlOp::Document => match arg(0)? {
255            Value::Null => Value::Null,
256            v => Value::Bool(well_formed(text_of(v)?, true, session)),
257        },
258    })
259}