Age Owner Branch data TLA Line data Source code
1 : : /*
2 : : * contrib/xml2/xpath.c
3 : : *
4 : : * Parser interface for DOM-based parser (libxml) rather than
5 : : * stream-based SAX-type parser
6 : : */
7 : : #include "postgres.h"
8 : :
9 : : #include "access/htup_details.h"
10 : : #include "executor/spi.h"
11 : : #include "fmgr.h"
12 : : #include "funcapi.h"
13 : : #include "lib/stringinfo.h"
14 : : #include "utils/builtins.h"
15 : : #include "utils/tuplestore.h"
16 : : #include "utils/xml.h"
17 : :
18 : : /* libxml includes */
19 : :
20 : : #include <libxml/xpath.h>
21 : : #include <libxml/tree.h>
22 : : #include <libxml/xmlmemory.h>
23 : : #include <libxml/xmlerror.h>
24 : : #include <libxml/parserInternals.h>
25 : :
461 tgl@sss.pgh.pa.us 26 :CBC 1 : PG_MODULE_MAGIC_EXT(
27 : : .name = "xml2",
28 : : .version = PG_VERSION
29 : : );
30 : :
31 : : /* exported for use by xslt_proc.c */
32 : :
33 : : PgXmlErrorContext *pgxml_parser_init(PgXmlStrictness strictness);
34 : :
35 : : /* workspace for pgxml_xpath() */
36 : :
37 : : typedef struct
38 : : {
39 : : xmlDocPtr doctree;
40 : : xmlXPathContextPtr ctxt;
41 : : xmlXPathObjectPtr res;
42 : : } xpath_workspace;
43 : :
44 : : /* local declarations */
45 : :
46 : : static xmlChar *pgxmlNodeSetToText(xmlNodeSetPtr nodeset,
47 : : xmlChar *toptagname, xmlChar *septagname,
48 : : xmlChar *plainsep);
49 : :
50 : : static text *pgxml_result_to_text(xmlXPathObjectPtr res, xmlChar *toptag,
51 : : xmlChar *septag, xmlChar *plainsep);
52 : :
53 : : static xmlChar *pgxml_texttoxmlchar(text *textstring);
54 : :
55 : : static xpath_workspace *pgxml_xpath(text *document, xmlChar *xpath,
56 : : PgXmlErrorContext *xmlerrcxt);
57 : :
58 : : static void cleanup_workspace(xpath_workspace *workspace);
59 : :
60 : :
61 : : /*
62 : : * Initialize for xml parsing.
63 : : *
64 : : * As with the underlying pg_xml_init function, calls to this MUST be followed
65 : : * by a PG_TRY block that guarantees that pg_xml_done is called.
66 : : */
67 : : PgXmlErrorContext *
5459 68 : 12 : pgxml_parser_init(PgXmlStrictness strictness)
69 : : {
70 : : PgXmlErrorContext *xmlerrcxt;
71 : :
72 : : /* Set up error handling (we share the core's error handler) */
73 : 12 : xmlerrcxt = pg_xml_init(strictness);
74 : :
75 : : /* Note: we're assuming an elog cannot be thrown by the following calls */
76 : :
77 : : /* Initialize libxml */
5966 78 : 12 : xmlInitParser();
79 : :
5459 80 : 12 : return xmlerrcxt;
81 : : }
82 : :
83 : :
84 : : /* Encodes special characters (<, >, &, " and \r) as XML entities */
85 : :
7880 bruce@momjian.us 86 : 1 : PG_FUNCTION_INFO_V1(xml_encode_special_chars);
87 : :
88 : : Datum
7880 bruce@momjian.us 89 :UBC 0 : xml_encode_special_chars(PG_FUNCTION_ARGS)
90 : : {
3397 noah@leadboat.com 91 : 0 : text *tin = PG_GETARG_TEXT_PP(0);
357 tgl@sss.pgh.pa.us 92 :UNC 0 : text *volatile tout = NULL;
93 : 0 : xmlChar *volatile tt = NULL;
94 : : PgXmlErrorContext *xmlerrcxt;
95 : :
364 michael@paquier.xyz 96 : 0 : xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL);
97 : :
98 [ # # ]: 0 : PG_TRY();
99 : : {
100 : : xmlChar *ts;
101 : :
102 : 0 : ts = pgxml_texttoxmlchar(tin);
103 : :
104 : 0 : tt = xmlEncodeSpecialChars(NULL, ts);
105 [ # # # # ]: 0 : if (tt == NULL || pg_xml_error_occurred(xmlerrcxt))
106 : 0 : xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
107 : : "could not allocate xmlChar");
108 : 0 : pfree(ts);
109 : :
110 : 0 : tout = cstring_to_text((char *) tt);
111 : : }
112 : 0 : PG_CATCH();
113 : : {
114 [ # # ]: 0 : if (tt != NULL)
357 tgl@sss.pgh.pa.us 115 : 0 : xmlFree(tt);
116 : :
364 michael@paquier.xyz 117 : 0 : pg_xml_done(xmlerrcxt, true);
118 : :
119 : 0 : PG_RE_THROW();
120 : : }
121 [ # # ]: 0 : PG_END_TRY();
122 : :
123 [ # # ]: 0 : if (tt != NULL)
357 tgl@sss.pgh.pa.us 124 : 0 : xmlFree(tt);
125 : :
364 michael@paquier.xyz 126 : 0 : pg_xml_done(xmlerrcxt, false);
127 : :
7880 bruce@momjian.us 128 :UBC 0 : PG_RETURN_TEXT_P(tout);
129 : : }
130 : :
131 : : /*
132 : : * Function translates a nodeset into a text representation
133 : : *
134 : : * iterates over each node in the set and calls xmlNodeDump to write it to
135 : : * an xmlBuffer -from which an xmlChar * string is returned.
136 : : *
137 : : * each representation is surrounded by <tagname> ... </tagname>
138 : : *
139 : : * plainsep is an ordinary (not tag) separator - if used, then nodes are
140 : : * cast to string as output method
141 : : */
142 : : static xmlChar *
8152 bruce@momjian.us 143 :CBC 6 : pgxmlNodeSetToText(xmlNodeSetPtr nodeset,
144 : : xmlChar *toptagname,
145 : : xmlChar *septagname,
146 : : xmlChar *plainsep)
147 : : {
364 michael@paquier.xyz 148 :GNC 6 : volatile xmlBufferPtr buf = NULL;
357 tgl@sss.pgh.pa.us 149 : 6 : xmlChar *volatile result = NULL;
27 michael@paquier.xyz 150 : 6 : xmlChar *volatile str = NULL;
151 : : PgXmlErrorContext *xmlerrcxt;
152 : :
153 : : /* spin up some error handling */
364 154 : 6 : xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL);
155 : :
156 [ + - ]: 6 : PG_TRY();
157 : : {
158 : 6 : buf = xmlBufferCreate();
159 : :
160 [ + - - + ]: 6 : if (buf == NULL || pg_xml_error_occurred(xmlerrcxt))
364 michael@paquier.xyz 161 :UNC 0 : xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
162 : : "could not allocate xmlBuffer");
163 : :
364 michael@paquier.xyz 164 [ + + + + ]:GNC 6 : if ((toptagname != NULL) && (xmlStrlen(toptagname) > 0))
165 : : {
166 : 1 : xmlBufferWriteChar(buf, "<");
167 : 1 : xmlBufferWriteCHAR(buf, toptagname);
168 : 1 : xmlBufferWriteChar(buf, ">");
169 : : }
170 [ + - ]: 6 : if (nodeset != NULL)
171 : : {
357 tgl@sss.pgh.pa.us 172 [ + + ]: 17 : for (int i = 0; i < nodeset->nodeNr; i++)
173 : : {
364 michael@paquier.xyz 174 [ + + ]: 11 : if (plainsep != NULL)
175 : : {
27 176 : 4 : str = xmlXPathCastNodeToString(nodeset->nodeTab[i]);
177 [ + - - + ]: 4 : if (str == NULL || pg_xml_error_occurred(xmlerrcxt))
27 michael@paquier.xyz 178 :UNC 0 : xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
179 : : "could not allocate node text");
180 : :
27 michael@paquier.xyz 181 :GNC 4 : xmlBufferWriteCHAR(buf, str);
182 : 4 : xmlFree(str);
183 : 4 : str = NULL;
184 : :
185 : : /* If this isn't the last entry, write the plain sep. */
364 186 [ + + ]: 4 : if (i < (nodeset->nodeNr) - 1)
187 : 2 : xmlBufferWriteChar(buf, (char *) plainsep);
188 : : }
189 : : else
190 : : {
19 191 : 7 : xmlNodePtr node = nodeset->nodeTab[i];
192 : :
364 193 [ + - + + ]: 7 : if ((septagname != NULL) && (xmlStrlen(septagname) > 0))
194 : : {
195 : 4 : xmlBufferWriteChar(buf, "<");
196 : 4 : xmlBufferWriteCHAR(buf, septagname);
197 : 4 : xmlBufferWriteChar(buf, ">");
198 : : }
199 : :
200 : : /*
201 : : * XML_NAMESPACE_DECL nodes are xmlNs structs, that cannot
202 : : * be processed by xmlNodeDump().
203 : : */
19 204 [ + + ]: 7 : if (node->type == XML_NAMESPACE_DECL)
205 : : {
206 : 1 : str = xmlXPathCastNodeToString(node);
207 [ + - - + ]: 1 : if (str == NULL || pg_xml_error_occurred(xmlerrcxt))
19 michael@paquier.xyz 208 :UNC 0 : xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
209 : : "could not allocate node text");
19 michael@paquier.xyz 210 :GNC 1 : xmlBufferWriteCHAR(buf, str);
211 : 1 : xmlFree(str);
212 : 1 : str = NULL;
213 : : }
214 : : else
215 : 6 : xmlNodeDump(buf, node->doc, node, 1, 0);
216 : :
364 217 [ + - + + ]: 7 : if ((septagname != NULL) && (xmlStrlen(septagname) > 0))
218 : : {
219 : 4 : xmlBufferWriteChar(buf, "</");
220 : 4 : xmlBufferWriteCHAR(buf, septagname);
221 : 4 : xmlBufferWriteChar(buf, ">");
222 : : }
223 : : }
224 : : }
225 : : }
226 : :
227 [ + + + + ]: 6 : if ((toptagname != NULL) && (xmlStrlen(toptagname) > 0))
228 : : {
229 : 1 : xmlBufferWriteChar(buf, "</");
230 : 1 : xmlBufferWriteCHAR(buf, toptagname);
231 : 1 : xmlBufferWriteChar(buf, ">");
232 : : }
233 : :
358 234 : 6 : result = xmlStrdup(xmlBufferContent(buf));
364 235 [ + - - + ]: 6 : if (result == NULL || pg_xml_error_occurred(xmlerrcxt))
364 michael@paquier.xyz 236 :UNC 0 : xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
237 : : "could not allocate result");
238 : : }
239 : 0 : PG_CATCH();
240 : : {
27 241 [ # # ]: 0 : if (result)
242 : 0 : xmlFree(result);
243 [ # # ]: 0 : if (str)
244 : 0 : xmlFree(str);
364 245 [ # # ]: 0 : if (buf)
246 : 0 : xmlBufferFree(buf);
247 : :
248 : 0 : pg_xml_done(xmlerrcxt, true);
249 : :
250 : 0 : PG_RE_THROW();
251 : : }
364 michael@paquier.xyz 252 [ - + ]:GNC 6 : PG_END_TRY();
253 : :
8152 bruce@momjian.us 254 :CBC 6 : xmlBufferFree(buf);
364 michael@paquier.xyz 255 :GNC 6 : pg_xml_done(xmlerrcxt, false);
256 : :
8152 bruce@momjian.us 257 :CBC 6 : return result;
258 : : }
259 : :
260 : :
261 : : /*
262 : : * Translate a PostgreSQL "varlena" -i.e. a variable length parameter
263 : : * into the libxml2 representation
264 : : */
265 : : static xmlChar *
266 : 16 : pgxml_texttoxmlchar(text *textstring)
267 : : {
6631 tgl@sss.pgh.pa.us 268 : 16 : return (xmlChar *) text_to_cstring(textstring);
269 : : }
270 : :
271 : : /* Publicly visible XPath functions */
272 : :
273 : : /*
274 : : * This is a "raw" xpath function. Check that it returns child elements
275 : : * properly
276 : : */
8152 bruce@momjian.us 277 : 2 : PG_FUNCTION_INFO_V1(xpath_nodeset);
278 : :
279 : : Datum
280 : 4 : xpath_nodeset(PG_FUNCTION_ARGS)
281 : : {
3397 noah@leadboat.com 282 : 4 : text *document = PG_GETARG_TEXT_PP(0);
3296 tgl@sss.pgh.pa.us 283 : 4 : text *xpathsupp = PG_GETARG_TEXT_PP(1); /* XPath expression */
3397 noah@leadboat.com 284 : 4 : xmlChar *toptag = pgxml_texttoxmlchar(PG_GETARG_TEXT_PP(2));
285 : 4 : xmlChar *septag = pgxml_texttoxmlchar(PG_GETARG_TEXT_PP(3));
286 : : xmlChar *xpath;
357 tgl@sss.pgh.pa.us 287 :GNC 4 : text *volatile xpres = NULL;
288 : 4 : xpath_workspace *volatile workspace = NULL;
289 : : PgXmlErrorContext *xmlerrcxt;
290 : :
5695 tgl@sss.pgh.pa.us 291 :CBC 4 : xpath = pgxml_texttoxmlchar(xpathsupp);
364 michael@paquier.xyz 292 :GNC 4 : xmlerrcxt = pgxml_parser_init(PG_XML_STRICTNESS_LEGACY);
293 : :
294 [ + - ]: 4 : PG_TRY();
295 : : {
296 : 4 : workspace = pgxml_xpath(document, xpath, xmlerrcxt);
297 : 4 : xpres = pgxml_result_to_text(workspace->res, toptag, septag, NULL);
298 : : }
364 michael@paquier.xyz 299 :UNC 0 : PG_CATCH();
300 : : {
301 [ # # ]: 0 : if (workspace)
302 : 0 : cleanup_workspace(workspace);
303 : :
304 : 0 : pg_xml_done(xmlerrcxt, true);
305 : 0 : PG_RE_THROW();
306 : : }
364 michael@paquier.xyz 307 [ - + ]:GNC 4 : PG_END_TRY();
308 : :
309 : 4 : cleanup_workspace(workspace);
310 : 4 : pg_xml_done(xmlerrcxt, false);
311 : :
7930 neilc@samurai.com 312 :CBC 4 : pfree(xpath);
313 : :
7975 bruce@momjian.us 314 [ - + ]: 4 : if (xpres == NULL)
7975 bruce@momjian.us 315 :UBC 0 : PG_RETURN_NULL();
8152 bruce@momjian.us 316 :CBC 4 : PG_RETURN_TEXT_P(xpres);
317 : : }
318 : :
319 : : /*
320 : : * The following function is almost identical, but returns the elements in
321 : : * a list.
322 : : */
323 : 2 : PG_FUNCTION_INFO_V1(xpath_list);
324 : :
325 : : Datum
326 : 2 : xpath_list(PG_FUNCTION_ARGS)
327 : : {
3397 noah@leadboat.com 328 : 2 : text *document = PG_GETARG_TEXT_PP(0);
3296 tgl@sss.pgh.pa.us 329 : 2 : text *xpathsupp = PG_GETARG_TEXT_PP(1); /* XPath expression */
3397 noah@leadboat.com 330 : 2 : xmlChar *plainsep = pgxml_texttoxmlchar(PG_GETARG_TEXT_PP(2));
331 : : xmlChar *xpath;
357 tgl@sss.pgh.pa.us 332 :GNC 2 : text *volatile xpres = NULL;
333 : 2 : xpath_workspace *volatile workspace = NULL;
334 : : PgXmlErrorContext *xmlerrcxt;
335 : :
5695 tgl@sss.pgh.pa.us 336 :CBC 2 : xpath = pgxml_texttoxmlchar(xpathsupp);
364 michael@paquier.xyz 337 :GNC 2 : xmlerrcxt = pgxml_parser_init(PG_XML_STRICTNESS_LEGACY);
338 : :
339 [ + - ]: 2 : PG_TRY();
340 : : {
341 : 2 : workspace = pgxml_xpath(document, xpath, xmlerrcxt);
342 : 2 : xpres = pgxml_result_to_text(workspace->res, NULL, NULL, plainsep);
343 : : }
364 michael@paquier.xyz 344 :UNC 0 : PG_CATCH();
345 : : {
346 [ # # ]: 0 : if (workspace)
347 : 0 : cleanup_workspace(workspace);
348 : :
349 : 0 : pg_xml_done(xmlerrcxt, true);
350 : 0 : PG_RE_THROW();
351 : : }
364 michael@paquier.xyz 352 [ - + ]:GNC 2 : PG_END_TRY();
353 : :
354 : 2 : cleanup_workspace(workspace);
355 : 2 : pg_xml_done(xmlerrcxt, false);
356 : :
7930 neilc@samurai.com 357 :CBC 2 : pfree(xpath);
358 : :
7975 bruce@momjian.us 359 [ - + ]: 2 : if (xpres == NULL)
7975 bruce@momjian.us 360 :UBC 0 : PG_RETURN_NULL();
8152 bruce@momjian.us 361 :CBC 2 : PG_RETURN_TEXT_P(xpres);
362 : : }
363 : :
364 : :
365 : 2 : PG_FUNCTION_INFO_V1(xpath_string);
366 : :
367 : : Datum
368 : 1 : xpath_string(PG_FUNCTION_ARGS)
369 : : {
3397 noah@leadboat.com 370 : 1 : text *document = PG_GETARG_TEXT_PP(0);
3296 tgl@sss.pgh.pa.us 371 : 1 : text *xpathsupp = PG_GETARG_TEXT_PP(1); /* XPath expression */
372 : : xmlChar *xpath;
373 : : int32 pathsize;
357 tgl@sss.pgh.pa.us 374 :GNC 1 : text *volatile xpres = NULL;
375 : 1 : xpath_workspace *volatile workspace = NULL;
376 : : PgXmlErrorContext *xmlerrcxt;
377 : :
3397 noah@leadboat.com 378 [ - + - - :CBC 1 : pathsize = VARSIZE_ANY_EXHDR(xpathsupp);
- - - - +
- ]
379 : :
380 : : /*
381 : : * We encapsulate the supplied path with "string()" = 8 chars + 1 for NUL
382 : : * at end
383 : : */
384 : : /* We could try casting to string using the libxml function? */
385 : :
7975 bruce@momjian.us 386 : 1 : xpath = (xmlChar *) palloc(pathsize + 9);
503 peter@eisentraut.org 387 : 1 : memcpy(xpath, "string(", 7);
388 [ + - ]: 1 : memcpy(xpath + 7, VARDATA_ANY(xpathsupp), pathsize);
7975 bruce@momjian.us 389 : 1 : xpath[pathsize + 7] = ')';
390 : 1 : xpath[pathsize + 8] = '\0';
391 : :
364 michael@paquier.xyz 392 :GNC 1 : xmlerrcxt = pgxml_parser_init(PG_XML_STRICTNESS_LEGACY);
393 : :
394 [ + - ]: 1 : PG_TRY();
395 : : {
396 : 1 : workspace = pgxml_xpath(document, xpath, xmlerrcxt);
397 : 1 : xpres = pgxml_result_to_text(workspace->res, NULL, NULL, NULL);
398 : : }
364 michael@paquier.xyz 399 :UNC 0 : PG_CATCH();
400 : : {
401 [ # # ]: 0 : if (workspace)
402 : 0 : cleanup_workspace(workspace);
403 : :
404 : 0 : pg_xml_done(xmlerrcxt, true);
405 : 0 : PG_RE_THROW();
406 : : }
364 michael@paquier.xyz 407 [ - + ]:GNC 1 : PG_END_TRY();
408 : :
409 : 1 : cleanup_workspace(workspace);
410 : 1 : pg_xml_done(xmlerrcxt, false);
411 : :
7930 neilc@samurai.com 412 :CBC 1 : pfree(xpath);
413 : :
7975 bruce@momjian.us 414 [ + - ]: 1 : if (xpres == NULL)
415 : 1 : PG_RETURN_NULL();
8152 bruce@momjian.us 416 :UBC 0 : PG_RETURN_TEXT_P(xpres);
417 : : }
418 : :
419 : :
8152 bruce@momjian.us 420 :CBC 1 : PG_FUNCTION_INFO_V1(xpath_number);
421 : :
422 : : Datum
8152 bruce@momjian.us 423 :UBC 0 : xpath_number(PG_FUNCTION_ARGS)
424 : : {
3397 noah@leadboat.com 425 : 0 : text *document = PG_GETARG_TEXT_PP(0);
3296 tgl@sss.pgh.pa.us 426 : 0 : text *xpathsupp = PG_GETARG_TEXT_PP(1); /* XPath expression */
427 : : xmlChar *xpath;
357 tgl@sss.pgh.pa.us 428 :UNC 0 : volatile float4 fRes = 0.0;
429 : 0 : volatile bool isNull = false;
430 : 0 : xpath_workspace *volatile workspace = NULL;
431 : : PgXmlErrorContext *xmlerrcxt;
432 : :
8152 bruce@momjian.us 433 :UBC 0 : xpath = pgxml_texttoxmlchar(xpathsupp);
364 michael@paquier.xyz 434 :UNC 0 : xmlerrcxt = pgxml_parser_init(PG_XML_STRICTNESS_LEGACY);
435 : :
436 [ # # ]: 0 : PG_TRY();
437 : : {
438 : 0 : workspace = pgxml_xpath(document, xpath, xmlerrcxt);
439 : 0 : pfree(xpath);
440 : :
441 [ # # ]: 0 : if (workspace->res == NULL)
442 : 0 : isNull = true;
443 : : else
444 : 0 : fRes = xmlXPathCastToNumber(workspace->res);
445 : : }
446 : 0 : PG_CATCH();
447 : : {
448 [ # # ]: 0 : if (workspace)
449 : 0 : cleanup_workspace(workspace);
450 : :
451 : 0 : pg_xml_done(xmlerrcxt, true);
452 : 0 : PG_RE_THROW();
453 : : }
454 [ # # ]: 0 : PG_END_TRY();
455 : :
456 : 0 : cleanup_workspace(workspace);
457 : 0 : pg_xml_done(xmlerrcxt, false);
458 : :
459 [ # # # # ]: 0 : if (isNull || xmlXPathIsNaN(fRes))
7975 bruce@momjian.us 460 :UBC 0 : PG_RETURN_NULL();
461 : :
8152 462 : 0 : PG_RETURN_FLOAT4(fRes);
463 : : }
464 : :
465 : :
8152 bruce@momjian.us 466 :CBC 1 : PG_FUNCTION_INFO_V1(xpath_bool);
467 : :
468 : : Datum
8152 bruce@momjian.us 469 :UBC 0 : xpath_bool(PG_FUNCTION_ARGS)
470 : : {
3397 noah@leadboat.com 471 : 0 : text *document = PG_GETARG_TEXT_PP(0);
3296 tgl@sss.pgh.pa.us 472 : 0 : text *xpathsupp = PG_GETARG_TEXT_PP(1); /* XPath expression */
473 : : xmlChar *xpath;
357 tgl@sss.pgh.pa.us 474 :UNC 0 : volatile int bRes = 0;
475 : 0 : xpath_workspace *volatile workspace = NULL;
476 : : PgXmlErrorContext *xmlerrcxt;
477 : :
8152 bruce@momjian.us 478 :UBC 0 : xpath = pgxml_texttoxmlchar(xpathsupp);
364 michael@paquier.xyz 479 :UNC 0 : xmlerrcxt = pgxml_parser_init(PG_XML_STRICTNESS_LEGACY);
480 : :
481 [ # # ]: 0 : PG_TRY();
482 : : {
483 : 0 : workspace = pgxml_xpath(document, xpath, xmlerrcxt);
484 : 0 : pfree(xpath);
485 : :
486 [ # # ]: 0 : if (workspace->res == NULL)
487 : 0 : bRes = 0;
488 : : else
489 : 0 : bRes = xmlXPathCastToBoolean(workspace->res);
490 : : }
491 : 0 : PG_CATCH();
492 : : {
493 [ # # ]: 0 : if (workspace)
494 : 0 : cleanup_workspace(workspace);
495 : :
496 : 0 : pg_xml_done(xmlerrcxt, true);
497 : 0 : PG_RE_THROW();
498 : : }
499 [ # # ]: 0 : PG_END_TRY();
500 : :
501 : 0 : cleanup_workspace(workspace);
502 : 0 : pg_xml_done(xmlerrcxt, false);
503 : :
8152 bruce@momjian.us 504 :UBC 0 : PG_RETURN_BOOL(bRes);
505 : : }
506 : :
507 : :
508 : :
509 : : /* Core function to evaluate XPath query */
510 : :
511 : : static xpath_workspace *
364 michael@paquier.xyz 512 :GNC 7 : pgxml_xpath(text *document, xmlChar *xpath, PgXmlErrorContext *xmlerrcxt)
513 : : {
3397 noah@leadboat.com 514 [ - + - - :CBC 7 : int32 docsize = VARSIZE_ANY_EXHDR(document);
- - - - +
+ ]
27 michael@paquier.xyz 515 :GNC 7 : xmlXPathCompExprPtr volatile comppath = NULL;
207 516 : 7 : xpath_workspace *workspace = palloc0_object(xpath_workspace);
517 : :
5695 tgl@sss.pgh.pa.us 518 :CBC 7 : workspace->doctree = NULL;
519 : 7 : workspace->ctxt = NULL;
520 : 7 : workspace->res = NULL;
521 : :
27 michael@paquier.xyz 522 [ + - ]: 7 : PG_TRY();
523 : : {
524 [ + + ]: 7 : workspace->doctree = xmlReadMemory((char *) VARDATA_ANY(document),
525 : : docsize, NULL, NULL,
526 : : XML_PARSE_NOENT);
527 [ + + ]: 7 : if (workspace->doctree != NULL)
528 : : {
529 : 6 : workspace->ctxt = xmlXPathNewContext(workspace->doctree);
27 michael@paquier.xyz 530 [ - + ]:GNC 6 : if (workspace->ctxt == NULL)
27 michael@paquier.xyz 531 :UNC 0 : xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
532 : : "could not allocate XPath context");
533 : :
27 michael@paquier.xyz 534 :CBC 6 : workspace->ctxt->node = xmlDocGetRootElement(workspace->doctree);
535 : :
536 : : /* compile the path */
537 : 6 : comppath = xmlXPathCtxtCompile(workspace->ctxt, xpath);
27 michael@paquier.xyz 538 [ + - - + ]:GNC 6 : if (comppath == NULL || pg_xml_error_occurred(xmlerrcxt))
27 michael@paquier.xyz 539 :UBC 0 : xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_ARGUMENT_FOR_XQUERY,
540 : : "XPath Syntax Error");
541 : :
542 : : /* Now evaluate the path expression. */
27 michael@paquier.xyz 543 :CBC 6 : workspace->res = xmlXPathCompiledEval(comppath, workspace->ctxt);
544 : :
545 : 6 : xmlXPathFreeCompExpr(comppath);
27 michael@paquier.xyz 546 :GNC 6 : comppath = NULL;
547 : : }
548 : : }
27 michael@paquier.xyz 549 :UBC 0 : PG_CATCH();
550 : : {
27 michael@paquier.xyz 551 [ # # ]:UNC 0 : if (comppath != NULL)
552 : 0 : xmlXPathFreeCompExpr(comppath);
27 michael@paquier.xyz 553 :UBC 0 : cleanup_workspace(workspace);
554 : :
555 : 0 : PG_RE_THROW();
556 : : }
27 michael@paquier.xyz 557 [ - + ]:CBC 7 : PG_END_TRY();
558 : :
364 michael@paquier.xyz 559 :GNC 7 : return workspace;
560 : : }
561 : :
562 : : /* Clean up after processing the result of pgxml_xpath() */
563 : : static void
357 tgl@sss.pgh.pa.us 564 :CBC 7 : cleanup_workspace(xpath_workspace *workspace)
565 : : {
5695 566 [ + + ]: 7 : if (workspace->res)
567 : 6 : xmlXPathFreeObject(workspace->res);
568 : 7 : workspace->res = NULL;
569 [ + + ]: 7 : if (workspace->ctxt)
570 : 6 : xmlXPathFreeContext(workspace->ctxt);
571 : 7 : workspace->ctxt = NULL;
572 [ + + ]: 7 : if (workspace->doctree)
573 : 6 : xmlFreeDoc(workspace->doctree);
574 : 7 : workspace->doctree = NULL;
575 : 7 : }
576 : :
577 : : static text *
7975 bruce@momjian.us 578 : 7 : pgxml_result_to_text(xmlXPathObjectPtr res,
579 : : xmlChar *toptag,
580 : : xmlChar *septag,
581 : : xmlChar *plainsep)
582 : : {
357 tgl@sss.pgh.pa.us 583 :GNC 7 : xmlChar *volatile xpresstr = NULL;
584 : 7 : text *volatile xpres = NULL;
585 : : PgXmlErrorContext *xmlerrcxt;
586 : :
7975 bruce@momjian.us 587 [ + + ]:CBC 7 : if (res == NULL)
588 : 1 : return NULL;
589 : :
590 : : /* spin some error handling */
364 michael@paquier.xyz 591 :GNC 6 : xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL);
592 : :
593 [ + - ]: 6 : PG_TRY();
594 : : {
595 [ + - - ]: 6 : switch (res->type)
596 : : {
597 : 6 : case XPATH_NODESET:
598 : 6 : xpresstr = pgxmlNodeSetToText(res->nodesetval,
599 : : toptag,
600 : : septag, plainsep);
601 : 6 : break;
602 : :
364 michael@paquier.xyz 603 :UNC 0 : case XPATH_STRING:
604 : 0 : xpresstr = xmlStrdup(res->stringval);
605 [ # # # # ]: 0 : if (xpresstr == NULL || pg_xml_error_occurred(xmlerrcxt))
606 : 0 : xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
607 : : "could not allocate result");
608 : 0 : break;
609 : :
610 : 0 : default:
611 [ # # ]: 0 : elog(NOTICE, "unsupported XQuery result: %d", res->type);
612 : 0 : xpresstr = xmlStrdup((const xmlChar *) "<unsupported/>");
613 [ # # # # ]: 0 : if (xpresstr == NULL || pg_xml_error_occurred(xmlerrcxt))
614 : 0 : xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
615 : : "could not allocate result");
616 : : }
617 : :
618 : : /* Now convert this result back to text */
364 michael@paquier.xyz 619 :GNC 6 : xpres = cstring_to_text((char *) xpresstr);
620 : : }
364 michael@paquier.xyz 621 :UNC 0 : PG_CATCH();
622 : : {
623 [ # # ]: 0 : if (xpresstr != NULL)
357 tgl@sss.pgh.pa.us 624 : 0 : xmlFree(xpresstr);
625 : :
364 michael@paquier.xyz 626 : 0 : pg_xml_done(xmlerrcxt, true);
627 : :
628 : 0 : PG_RE_THROW();
629 : : }
364 michael@paquier.xyz 630 [ - + ]:GNC 6 : PG_END_TRY();
631 : :
632 : : /* Free various storage */
357 tgl@sss.pgh.pa.us 633 :CBC 6 : xmlFree(xpresstr);
634 : :
364 michael@paquier.xyz 635 :GNC 6 : pg_xml_done(xmlerrcxt, false);
636 : :
8152 bruce@momjian.us 637 :CBC 6 : return xpres;
638 : : }
639 : :
640 : : /*
641 : : * xpath_table is a table function. It needs some tidying (as do the
642 : : * other functions here!
643 : : */
644 : 2 : PG_FUNCTION_INFO_V1(xpath_table);
645 : :
646 : : Datum
7975 647 : 5 : xpath_table(PG_FUNCTION_ARGS)
648 : : {
649 : : /* Function parameters */
5966 tgl@sss.pgh.pa.us 650 : 5 : char *pkeyfield = text_to_cstring(PG_GETARG_TEXT_PP(0));
651 : 5 : char *xmlfield = text_to_cstring(PG_GETARG_TEXT_PP(1));
652 : 5 : char *relname = text_to_cstring(PG_GETARG_TEXT_PP(2));
653 : 5 : char *xpathset = text_to_cstring(PG_GETARG_TEXT_PP(3));
654 : 5 : char *condition = text_to_cstring(PG_GETARG_TEXT_PP(4));
655 : :
656 : : /* SPI (input tuple) support */
657 : : SPITupleTable *tuptable;
658 : : HeapTuple spi_tuple;
659 : : TupleDesc spi_tupdesc;
660 : :
661 : :
7975 bruce@momjian.us 662 : 5 : ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
663 : : AttInMetadata *attinmeta;
664 : :
665 : : char **values;
666 : : xmlChar **xpaths;
667 : : char *pos;
6925 tgl@sss.pgh.pa.us 668 : 5 : const char *pathsep = "|";
669 : :
670 : : int numpaths;
671 : : int ret;
672 : : uint64 proc;
673 : : int j;
674 : : int rownr; /* For issuing multiple rows from one original
675 : : * document */
676 : : bool had_values; /* To determine end of nodeset results */
677 : : StringInfoData query_buf;
678 : : PgXmlErrorContext *xmlerrcxt;
5459 679 : 5 : volatile xmlDocPtr doctree = NULL;
27 michael@paquier.xyz 680 :GNC 5 : xmlXPathContextPtr volatile ctxt = NULL;
681 : 5 : xmlXPathObjectPtr volatile res = NULL;
682 : 5 : xmlXPathCompExprPtr volatile comppath = NULL;
683 : 5 : xmlChar *volatile resstr = NULL;
684 : :
1351 michael@paquier.xyz 685 :CBC 5 : InitMaterializedSRF(fcinfo, MAT_SRF_USE_EXPECTED_DESC);
686 : :
687 : : /* must have at least one output column (for the pkey) */
1575 688 [ - + ]: 5 : if (rsinfo->setDesc->natts < 1)
5966 tgl@sss.pgh.pa.us 689 [ # # ]:UBC 0 : ereport(ERROR,
690 : : (errcode(ERRCODE_SYNTAX_ERROR),
691 : : errmsg("xpath_table must have at least one output column")));
692 : :
693 : : /*
694 : : * At the moment we assume that the returned attributes make sense for the
695 : : * XPath specified (i.e. we trust the caller). It's not fatal if they get
696 : : * it wrong - the input function for the column type will raise an error
697 : : * if the path result can't be converted into the correct binary
698 : : * representation.
699 : : */
700 : :
1575 michael@paquier.xyz 701 :CBC 5 : attinmeta = TupleDescGetAttInMetadata(rsinfo->setDesc);
702 : :
27 michael@paquier.xyz 703 :GNC 5 : values = (char **) palloc0(rsinfo->setDesc->natts * sizeof(char *));
1575 michael@paquier.xyz 704 :CBC 5 : xpaths = (xmlChar **) palloc(rsinfo->setDesc->natts * sizeof(xmlChar *));
705 : :
706 : : /*
707 : : * Split XPaths. xpathset is a writable CString.
708 : : *
709 : : * Note that we stop splitting once we've done all needed for tupdesc
710 : : */
7975 bruce@momjian.us 711 : 5 : numpaths = 0;
712 : 5 : pos = xpathset;
1575 michael@paquier.xyz 713 [ + + ]: 7 : while (numpaths < (rsinfo->setDesc->natts - 1))
714 : : {
5966 tgl@sss.pgh.pa.us 715 : 5 : xpaths[numpaths++] = (xmlChar *) pos;
7975 bruce@momjian.us 716 : 5 : pos = strstr(pos, pathsep);
717 [ + + ]: 5 : if (pos != NULL)
718 : : {
719 : 2 : *pos = '\0';
720 : 2 : pos++;
721 : : }
722 : : else
5966 tgl@sss.pgh.pa.us 723 : 3 : break;
724 : : }
725 : :
726 : : /* Now build query */
7426 neilc@samurai.com 727 : 5 : initStringInfo(&query_buf);
728 : :
729 : : /* Build initial sql statement */
730 : 5 : appendStringInfo(&query_buf, "SELECT %s, %s FROM %s WHERE %s",
731 : : pkeyfield,
732 : : xmlfield,
733 : : relname,
734 : : condition);
735 : :
659 tgl@sss.pgh.pa.us 736 : 5 : SPI_connect();
737 : :
7426 neilc@samurai.com 738 [ - + ]: 5 : if ((ret = SPI_exec(query_buf.data, 0)) != SPI_OK_SELECT)
5966 tgl@sss.pgh.pa.us 739 [ # # ]:UBC 0 : elog(ERROR, "xpath_table: SPI execution failed for query %s",
740 : : query_buf.data);
741 : :
7975 bruce@momjian.us 742 :CBC 5 : proc = SPI_processed;
743 : 5 : tuptable = SPI_tuptable;
744 : 5 : spi_tupdesc = tuptable->tupdesc;
745 : :
746 : : /*
747 : : * Check that SPI returned correct result. If you put a comma into one of
748 : : * the function parameters, this will catch it when the SPI query returns
749 : : * e.g. 3 columns.
750 : : */
751 [ - + ]: 5 : if (spi_tupdesc->natts != 2)
752 : : {
7975 bruce@momjian.us 753 [ # # ]:UBC 0 : ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
754 : : errmsg("expression returning multiple columns is not valid in parameter list"),
755 : : errdetail("Expected two columns in SPI result, got %d.", spi_tupdesc->natts)));
756 : : }
757 : :
758 : : /*
759 : : * Setup the parser. This should happen after we are done evaluating the
760 : : * query, in case it calls functions that set up libxml differently.
761 : : */
5459 tgl@sss.pgh.pa.us 762 :CBC 5 : xmlerrcxt = pgxml_parser_init(PG_XML_STRICTNESS_LEGACY);
763 : :
764 [ + - ]: 5 : PG_TRY();
765 : : {
766 : : /* For each row i.e. document returned from SPI */
767 : : uint64 i;
768 : :
5133 bruce@momjian.us 769 [ + + ]: 10 : for (i = 0; i < proc; i++)
770 : : {
771 : : char *pkey;
772 : : char *xmldoc;
773 : : HeapTuple ret_tuple;
774 : :
775 : : /* Extract the row data as C Strings */
776 : 5 : spi_tuple = tuptable->vals[i];
777 : 5 : pkey = SPI_getvalue(spi_tuple, spi_tupdesc, 1);
778 : 5 : xmldoc = SPI_getvalue(spi_tuple, spi_tupdesc, 2);
779 : :
780 : : /*
781 : : * Clear the values array, so that not-well-formed documents
782 : : * return NULL in all columns. Note that this also means that
783 : : * spare columns will be NULL.
784 : : */
1575 michael@paquier.xyz 785 [ + + ]: 15 : for (j = 0; j < rsinfo->setDesc->natts; j++)
5133 bruce@momjian.us 786 : 10 : values[j] = NULL;
787 : :
788 : : /* Insert primary key */
789 : 5 : values[0] = pkey;
790 : :
791 : : /* Parse the document */
792 [ + - ]: 5 : if (xmldoc)
895 michael@paquier.xyz 793 : 5 : doctree = xmlReadMemory(xmldoc, strlen(xmldoc),
794 : : NULL, NULL,
795 : : XML_PARSE_NOENT);
796 : : else /* treat NULL as not well-formed */
5133 bruce@momjian.us 797 :UBC 0 : doctree = NULL;
798 : :
5133 bruce@momjian.us 799 [ - + ]:CBC 5 : if (doctree == NULL)
800 : : {
801 : : /* not well-formed, so output all-NULL tuple */
5133 bruce@momjian.us 802 :UBC 0 : ret_tuple = BuildTupleFromCStrings(attinmeta, values);
1575 michael@paquier.xyz 803 : 0 : tuplestore_puttuple(rsinfo->setResult, ret_tuple);
5133 bruce@momjian.us 804 : 0 : heap_freetuple(ret_tuple);
805 : : }
806 : : else
807 : : {
808 : : /* New loop here - we have to deal with nodeset results */
5133 bruce@momjian.us 809 :CBC 5 : rownr = 0;
810 : :
811 : : do
812 : : {
813 : : /* Now evaluate the set of xpaths. */
814 : 8 : had_values = false;
815 [ + + ]: 18 : for (j = 0; j < numpaths; j++)
816 : : {
27 michael@paquier.xyz 817 :GNC 10 : ctxt = NULL;
818 : 10 : res = NULL;
819 : 10 : comppath = NULL;
820 : 10 : resstr = NULL;
821 : :
5133 bruce@momjian.us 822 :CBC 10 : ctxt = xmlXPathNewContext(doctree);
364 michael@paquier.xyz 823 [ + - - + ]:GNC 10 : if (ctxt == NULL || pg_xml_error_occurred(xmlerrcxt))
364 michael@paquier.xyz 824 :UNC 0 : xml_ereport(xmlerrcxt,
825 : : ERROR, ERRCODE_OUT_OF_MEMORY,
826 : : "could not allocate XPath context");
827 : :
5133 bruce@momjian.us 828 :CBC 10 : ctxt->node = xmlDocGetRootElement(doctree);
829 : :
830 : : /* compile the path */
653 tgl@sss.pgh.pa.us 831 : 10 : comppath = xmlXPathCtxtCompile(ctxt, xpaths[j]);
364 michael@paquier.xyz 832 [ + - - + ]:GNC 10 : if (comppath == NULL || pg_xml_error_occurred(xmlerrcxt))
5133 bruce@momjian.us 833 :UBC 0 : xml_ereport(xmlerrcxt, ERROR,
834 : : ERRCODE_INVALID_ARGUMENT_FOR_XQUERY,
835 : : "XPath Syntax Error");
836 : :
837 : : /* Now evaluate the path expression. */
5133 bruce@momjian.us 838 :CBC 10 : res = xmlXPathCompiledEval(comppath, ctxt);
839 : 10 : xmlXPathFreeCompExpr(comppath);
27 michael@paquier.xyz 840 :GNC 10 : comppath = NULL;
841 : :
5133 bruce@momjian.us 842 [ + - ]:CBC 10 : if (res != NULL)
843 : : {
844 [ + - - ]: 10 : switch (res->type)
845 : : {
846 : 10 : case XPATH_NODESET:
847 : : /* We see if this nodeset has enough nodes */
848 [ + - ]: 10 : if (res->nodesetval != NULL &&
849 [ + + ]: 10 : rownr < res->nodesetval->nodeNr)
850 : : {
851 : 4 : resstr = xmlXPathCastNodeToString(res->nodesetval->nodeTab[rownr]);
364 michael@paquier.xyz 852 [ + - - + ]:GNC 4 : if (resstr == NULL || pg_xml_error_occurred(xmlerrcxt))
364 michael@paquier.xyz 853 :UNC 0 : xml_ereport(xmlerrcxt,
854 : : ERROR, ERRCODE_OUT_OF_MEMORY,
855 : : "could not allocate result");
5133 bruce@momjian.us 856 :CBC 4 : had_values = true;
857 : : }
858 : : else
859 : 6 : resstr = NULL;
860 : :
861 : 10 : break;
862 : :
5133 bruce@momjian.us 863 :UBC 0 : case XPATH_STRING:
864 : 0 : resstr = xmlStrdup(res->stringval);
364 michael@paquier.xyz 865 [ # # # # ]:UNC 0 : if (resstr == NULL || pg_xml_error_occurred(xmlerrcxt))
866 : 0 : xml_ereport(xmlerrcxt,
867 : : ERROR, ERRCODE_OUT_OF_MEMORY,
868 : : "could not allocate result");
5133 bruce@momjian.us 869 :UBC 0 : break;
870 : :
871 : 0 : default:
872 [ # # ]: 0 : elog(NOTICE, "unsupported XQuery result: %d", res->type);
873 : 0 : resstr = xmlStrdup((const xmlChar *) "<unsupported/>");
364 michael@paquier.xyz 874 [ # # # # ]:UNC 0 : if (resstr == NULL || pg_xml_error_occurred(xmlerrcxt))
875 : 0 : xml_ereport(xmlerrcxt,
876 : : ERROR, ERRCODE_OUT_OF_MEMORY,
877 : : "could not allocate result");
878 : : }
879 : :
880 : : /*
881 : : * Insert this into the appropriate column in the
882 : : * result tuple.
883 : : */
5133 bruce@momjian.us 884 :CBC 10 : values[j + 1] = (char *) resstr;
27 michael@paquier.xyz 885 :GNC 10 : resstr = NULL;
886 : : }
887 : :
888 [ + - ]: 10 : if (res != NULL)
889 : : {
890 : 10 : xmlXPathFreeObject(res);
891 : 10 : res = NULL;
892 : : }
5133 bruce@momjian.us 893 :CBC 10 : xmlXPathFreeContext(ctxt);
27 michael@paquier.xyz 894 :GNC 10 : ctxt = NULL;
895 : : }
896 : :
897 : : /* Now add the tuple to the output, if there is one. */
5133 bruce@momjian.us 898 [ + + ]:CBC 8 : if (had_values)
899 : : {
900 : 3 : ret_tuple = BuildTupleFromCStrings(attinmeta, values);
1575 michael@paquier.xyz 901 : 3 : tuplestore_puttuple(rsinfo->setResult, ret_tuple);
5133 bruce@momjian.us 902 : 3 : heap_freetuple(ret_tuple);
903 : : }
904 : :
905 : : /* BuildTupleFromCStrings() has copied the values. */
27 michael@paquier.xyz 906 [ + + ]:GNC 18 : for (j = 1; j < rsinfo->setDesc->natts; j++)
907 : : {
908 [ + + ]: 10 : if (values[j] != NULL)
909 : : {
910 : 4 : xmlFree((xmlChar *) values[j]);
911 : 4 : values[j] = NULL;
912 : : }
913 : : }
914 : :
5133 bruce@momjian.us 915 :CBC 8 : rownr++;
916 [ + + ]: 8 : } while (had_values);
917 : : }
918 : :
919 [ + - ]: 5 : if (doctree != NULL)
920 : 5 : xmlFreeDoc(doctree);
921 : 5 : doctree = NULL;
922 : :
923 [ + - ]: 5 : if (pkey)
924 : 5 : pfree(pkey);
925 [ + - ]: 5 : if (xmldoc)
926 : 5 : pfree(xmldoc);
927 : : }
928 : : }
5459 tgl@sss.pgh.pa.us 929 :UBC 0 : PG_CATCH();
930 : : {
27 michael@paquier.xyz 931 [ # # ]:UNC 0 : if (resstr != NULL)
932 : 0 : xmlFree(resstr);
933 [ # # ]: 0 : for (j = 1; j < rsinfo->setDesc->natts; j++)
934 : : {
935 [ # # ]: 0 : if (values[j] != NULL)
936 : 0 : xmlFree((xmlChar *) values[j]);
937 : : }
938 [ # # ]: 0 : if (res != NULL)
939 : 0 : xmlXPathFreeObject(res);
940 [ # # ]: 0 : if (comppath != NULL)
941 : 0 : xmlXPathFreeCompExpr(comppath);
942 [ # # ]: 0 : if (ctxt != NULL)
943 : 0 : xmlXPathFreeContext(ctxt);
5459 tgl@sss.pgh.pa.us 944 [ # # ]:UBC 0 : if (doctree != NULL)
945 : 0 : xmlFreeDoc(doctree);
946 : :
947 : 0 : pg_xml_done(xmlerrcxt, true);
948 : :
949 : 0 : PG_RE_THROW();
950 : : }
5459 tgl@sss.pgh.pa.us 951 [ - + ]:CBC 5 : PG_END_TRY();
952 : :
953 [ - + ]: 5 : if (doctree != NULL)
5459 tgl@sss.pgh.pa.us 954 :UBC 0 : xmlFreeDoc(doctree);
955 : :
5459 tgl@sss.pgh.pa.us 956 :CBC 5 : pg_xml_done(xmlerrcxt, false);
957 : :
7975 bruce@momjian.us 958 : 5 : SPI_finish();
959 : :
960 : : /*
961 : : * SFRM_Materialize mode expects us to return a NULL Datum. The actual
962 : : * tuples are in our tuplestore and passed back through rsinfo->setResult.
963 : : * rsinfo->setDesc is set to the tuple description that we actually used
964 : : * to build our tuples with, so the caller can verify we did what it was
965 : : * expecting.
966 : : */
967 : 5 : return (Datum) 0;
968 : : }
|