Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
LibXML2.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
6#include <list>
7
8#include <libxml/xmlmemory.h>
9#include <libxml/xmlsave.h>
10
12#include "Stroika/Foundation/DataExchange/BadFormatException.h"
14#include "Stroika/Foundation/Execution/Throw.h"
16#include "Stroika/Foundation/Memory/Common.h"
19
20#include "LibXML2.h"
21
22using namespace Stroika::Foundation;
25using namespace Stroika::Foundation::DataExchange::XML;
26using namespace Stroika::Foundation::DataExchange::XML::DOM;
27using namespace Stroika::Foundation::DataExchange::XML::Schema;
28using namespace Stroika::Foundation::DataExchange::XML::Providers::LibXML2;
29using namespace Stroika::Foundation::Debug;
30using namespace Stroika::Foundation::Execution;
31using namespace Stroika::Foundation::Streams;
32
33using std::byte;
34
35using Memory::MakeSharedPtr;
36
37// Comment this in to turn on aggressive noisy DbgTrace in this module
38//#define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
39
40static_assert (qStroika_HasComponent_libxml2, "Don't compile this file if qStroika_HasComponent_libxml2 not set");
41
42CompileTimeFlagChecker_SOURCE (Stroika::Foundation::DataExchange::XML, qStroika_HasComponent_libxml2, qStroika_HasComponent_libxml2);
43
44// a schema compile's own resource loader (xmlSchemaSetResourceLoader) - so no process-global input callbacks - is libxml2 2.14's
45static_assert (LIBXML_VERSION >= 21400, "Stroika needs libxml2 2.14 or later - configure --libxml2 use (the default) builds one");
46
47namespace {
48 // From https://www.w3.org/TR/xml-names/
49 // In a namespace declaration, the URI reference is the normalized value of the attribute, so replacement of XML
50 // character and entity references has already been done before any comparison.
51 //
52 // Not 100% sure, but I think that means decode %x stuff too (at least that fixes bug I'm encountering with ASTM-CCR files)
53 // --LGP 2024-01-31
54 constexpr auto kUseURIEncodingFlag_ = URI::StringPCTEncodedFlag::eDecoded;
55}
56
57namespace {
58 /*
59 * What a schema compile loads - an xs:import's or xs:include's schemaLocation - through its Resource::ResolverPtr, as that
60 * compile's own loader (xmlSchemaSetResourceLoader): nothing process-global, so neither another thread nor another user of
61 * libxml2 in the process sees it. What the resolver does not have is loaded as libxml2 would, from its URL.
62 */
63 xmlParserErrors ResourceLoader_ (void* resolver, const char* url, const char* publicID, [[maybe_unused]] xmlResourceType type,
64 xmlParserInputFlags flags, xmlParserInput** out) noexcept
65 {
66 RequireNotNull (resolver);
67 RequireNotNull (url);
68 RequireNotNull (out);
69 try {
70 // libxml2 gives no import's namespace here, so the URL is each of the resolver's keys - but a public ID, where there is one
71 String u = String::FromUTF8 (url);
72 if (optional<Resource::Definition> r = reinterpret_cast<const Resource::ResolverPtr*> (resolver)->Lookup (
73 Resource::Name{.fNamespace = u, .fPublicID = publicID == nullptr ? u : String::FromUTF8 (publicID), .fSystemID = u})) {
74 *out = xmlNewInputFromMemory (url, r->fData.data (), r->fData.size (), flags);
75 return *out == nullptr ? XML_ERR_NO_MEMORY : XML_ERR_OK;
76 }
77 return xmlNewInputFromUrl (url, flags, out);
78 }
79 catch (...) {
80 // nothing may be thrown through libxml2, a C library
81 DbgTrace ("resolving '{}' failed: {}"_f, String::FromUTF8 (url), current_exception ());
82 return XML_ERR_INTERNAL_ERROR;
83 }
84 }
85}
86
87namespace {
88 /*
89 * What libxml2 reports to a schema compile or validation, as its structured error handler (xmlSchemaSetParserStructuredErrors,
90 * xmlSchemaSetValidStructuredErrors): the first error - what it says, and where - for the BadFormatException that follows;
91 * warnings are only traced. Given no handler, libxml2 prints its diagnostics to stderr, which is not a library's business.
92 * Not the generic pair of callbacks (xmlSchemaSetParserErrors, xmlSchemaSetValidErrors - deprecated as of libxml2 2.15):
93 * libxml2 passes only a structured handler on to the parser of each document an xs:import or xs:include loads
94 * (xmlSchemaAddSchemaDoc), so with the pair, what that parser reports - a schemaLocation that would not load - goes to
95 * stderr; and an xmlError's message is UTF-8, where the pair's printf-style arguments say nothing of their character set.
96 */
97 struct SchemaErrors_ {
98 String fWhatFailed; // the start of the exception's message
99 String fFirstError;
100 optional<unsigned int> fLine;
101 optional<unsigned int> fColumn;
102
103 static void Handler (void* userData, const xmlError* error) noexcept
104 {
105 auto useThis = reinterpret_cast<SchemaErrors_*> (userData);
106 try {
107 if (error->level == XML_ERR_WARNING) {
108 DbgTrace ("libxml2 warning ignored: {}"_f, String::FromUTF8 (error->message).Trim ());
109 }
110 else if (useThis->fFirstError.empty ()) {
111 useThis->fFirstError = String::FromUTF8 (error->message).Trim ();
112 if (error->line > 0) {
113 useThis->fLine = static_cast<unsigned int> (error->line);
114 }
115 if (error->int2 > 0) {
116 useThis->fColumn = static_cast<unsigned int> (error->int2);
117 }
118 }
119 }
120 catch (...) {
121 // nothing may be thrown through libxml2, a C library
122 }
123 }
124 BadFormatException AsException () const
125 {
126 return BadFormatException{fFirstError.empty () ? fWhatFailed : fWhatFailed + ": "_k + fFirstError, fLine, fColumn, nullopt};
127 }
128 };
129}
130
131namespace {
132 struct SchemaRep_ final : ILibXML2SchemaRep {
133#if qStroika_Foundation_DataExchange_XML_DebugMemoryAllocations
134 static inline atomic<unsigned int> sLiveCnt{0};
135#endif
136 SchemaRep_ (const shared_ptr<const void>& library, const Streams::InputStream::Ptr<byte>& schemaData, const Resource::ResolverPtr& resolver)
137 : fLibrary_{library}
138 , fResolver_{resolver}
139 , fSchemaData{schemaData.ReadAll ()}
140 {
141 xmlSchemaParserCtxt* schemaParseContext =
142 xmlSchemaNewMemParserCtxt (reinterpret_cast<const char*> (fSchemaData.data ()), static_cast<int> (fSchemaData.size ()));
143 Execution::ThrowIfNull (schemaParseContext); // this one IS an allocation, so bad_alloc is what it means
144 if (fResolver_ != nullptr) {
145 xmlSchemaSetResourceLoader (schemaParseContext, &ResourceLoader_, &fResolver_);
146 }
147 /*
148 * What is WRONG with the schema comes only through the handler: xmlSchemaParse answers just nullptr - which before
149 * Stroika v3.0d25 went to Execution::ThrowIfNull, and so came out as std::bad_alloc.
150 */
151 SchemaErrors_ errors{.fWhatFailed = "Failed to compile schema"_k};
152 xmlSchemaSetParserStructuredErrors (schemaParseContext, &SchemaErrors_::Handler, &errors);
153 fCompiledSchema = xmlSchemaParse (schemaParseContext);
154 xmlSchemaFreeParserCtxt (schemaParseContext);
155 if (fCompiledSchema == nullptr) [[unlikely]] {
156 Execution::Throw (errors.AsException ());
157 }
158#if qStroika_Foundation_DataExchange_XML_DebugMemoryAllocations
159 ++sLiveCnt;
160#endif
161 }
162 SchemaRep_ (const SchemaRep_&) = delete;
163 virtual ~SchemaRep_ ()
164 {
165 xmlSchemaFree (fCompiledSchema);
166#if qStroika_Foundation_DataExchange_XML_DebugMemoryAllocations
167 Assert (sLiveCnt > 0);
168 --sLiveCnt;
169#endif
170 }
171 shared_ptr<const void> fLibrary_; // first, so destroyed last: libxml2 lasts while anything of it does
172 Resource::ResolverPtr fResolver_{nullptr};
173 Memory::BLOB fSchemaData;
174 xmlSchema* fCompiledSchema{nullptr};
175
176 virtual const Providers::ISchemaProvider* GetProvider () const override
177 {
178 return &XML::Providers::LibXML2::kDefaultProvider;
179 }
180 virtual optional<URI> GetTargetNamespace () const override
181 {
182 Assert (fCompiledSchema != nullptr);
183 if (fCompiledSchema->targetNamespace != nullptr) {
184 return URI{libXMLString2String (fCompiledSchema->targetNamespace)};
185 }
186 return nullopt;
187 }
188 virtual Memory::BLOB GetData () override
189 {
190 return fSchemaData;
191 }
192 // not super useful, except if you want to clone
193 virtual Resource::ResolverPtr GetResolver () override
194 {
195 return fResolver_;
196 }
197 virtual xmlSchema* GetSchemaLibRep () override
198 {
199 AssertNotNull (fCompiledSchema);
200 return fCompiledSchema;
201 }
202 };
203}
204
205namespace {
206 struct SAXReader_ final {
207 using Name = StructuredStreamEvents::Name;
208 xmlSAXHandler flibXMLSaxHndler_{};
210 xmlParserCtxtPtr fCtxt_{nullptr}; // SAXParse sets this, as soon as it has made the context
211 // the consumer's exception, as thrown - so rethrown whole: a throw of *pointer-to-base would slice it
212 exception_ptr fCapturedException_;
213
214 /**
215 * Call an IConsumer from a libxml2 SAX callback. An IConsumer may throw - ObjectReader does, on data it cannot
216 * map - and the exception must not escape into libxml2, whose frames lie between here and SAXParse. libxml2 is
217 * C, so its functions are nounwind, and with -flto the unwind tables for them go away: the unwinder then finds
218 * no handler at all and terminates. (Without LTO it unwinds, which is why this stood for so long.) So catch it
219 * here, stop the parse, and let SAXParse rethrow it once libxml2's frames are gone.
220 */
221 template <typename CALL_CONSUMER>
222 static void Invoke_ (void* ctx, CALL_CONSUMER&& callConsumer) noexcept
223 {
224 SAXReader_* thisReader = reinterpret_cast<SAXReader_*> (ctx);
225 Assert (thisReader->flibXMLSaxHndler_.initialized == XML_SAX2_MAGIC); // assure ctx ptr passed through properly
226 if (thisReader->fCapturedException_ == nullptr) [[likely]] {
227 try {
228 callConsumer (thisReader->fCallback_);
229 }
230 catch (...) {
231 thisReader->fCapturedException_ = current_exception ();
232 xmlStopParser (thisReader->fCtxt_);
233 }
234 }
235 }
236 void ThrowIfCapturedException ()
237 {
238 if (fCapturedException_ != nullptr) [[unlikely]] {
239 rethrow_exception (fCapturedException_);
240 }
241 }
242
243 SAXReader_ (StructuredStreamEvents::IConsumer& callback)
244 : fCallback_{callback}
245 {
246 flibXMLSaxHndler_.initialized = XML_SAX2_MAGIC;
247 flibXMLSaxHndler_.startDocument = [] (void* ctx) {
248 Invoke_ (ctx, [] (StructuredStreamEvents::IConsumer& consumer) { consumer.StartDocument (); });
249 };
250 flibXMLSaxHndler_.endDocument = [] (void* ctx) {
251 Invoke_ (ctx, [] (StructuredStreamEvents::IConsumer& consumer) { consumer.EndDocument (); });
252 };
253 flibXMLSaxHndler_.startElementNs = [] (void* ctx, const xmlChar* localname, [[maybe_unused]] const xmlChar* prefix, const xmlChar* URI,
254 [[maybe_unused]] int nb_namespaces, [[maybe_unused]] const xmlChar** namespaces,
255 int nb_attributes, [[maybe_unused]] int nb_defaulted, const xmlChar** attributes) {
256 Invoke_ (ctx, [&] (StructuredStreamEvents::IConsumer& consumer) {
258 if (attributes != nullptr) {
259 // Crazy way to decode attribute arguments - I would have never guessed --
260 // https://stackoverflow.com/questions/2075894/how-to-get-the-name-and-value-of-attributes-from-xml-when-using-libxml2-sax-pars
261 auto ai = attributes;
262 for (int i = 0; i < nb_attributes; ++i) {
263 // @todo fix must grab namespace, probably from namespaces, but will take some research to figure out how!!!
264 // libxml2 hands over the value decoded - except that, not replacing entities (which would expand external
265 // ones too), it leaves each '&' it decoded as the reference "&#38;"
266 attrs.Add (Name{"", libXMLString2String (ai[0]), Name::eAttribute},
267 libXMLString2String (ai[3], static_cast<int> (ai[4] - ai[3])).ReplaceAll ("&#38;"sv, "&"sv));
268 ai += 5;
269 }
270 }
271 if (URI == nullptr) {
272 consumer.StartElement (Name{libXMLString2String (localname)}, attrs);
273 }
274 else {
275 consumer.StartElement (Name{libXMLString2String (URI), libXMLString2String (localname)}, attrs);
276 }
277 });
278 };
279 flibXMLSaxHndler_.endElementNs = [] (void* ctx, const xmlChar* localname, [[maybe_unused]] const xmlChar* prefix, const xmlChar* URI) {
280 Invoke_ (ctx, [&] (StructuredStreamEvents::IConsumer& consumer) {
281 if (URI == nullptr) {
282 consumer.EndElement (Name{libXMLString2String (localname)});
283 }
284 else {
285 consumer.EndElement (Name{libXMLString2String (URI), libXMLString2String (localname)});
286 }
287 });
288 };
289 flibXMLSaxHndler_.characters = [] (void* ctx, const xmlChar* ch, int len) {
290 Invoke_ (ctx, [&] (StructuredStreamEvents::IConsumer& consumer) {
291 // unclear how this handles 'continuations' and partial characters - may need a more sophisticated approach
292 consumer.TextInsideElement (String::FromUTF8 (span{reinterpret_cast<const char*> (ch), static_cast<size_t> (len)}));
293 });
294 };
295 }
296 SAXReader_ (const SAXReader_&) = delete;
297 };
298}
299
300namespace {
301#if qStroika_Foundation_Debug_AssertionsChecked
302 bool ValidNewNodeName_ (const String& n)
303 {
304 if (n.empty ()) {
305 return false;
306 }
307 if (n.find (':') != wstring::npos) { // if triggered, you probably used XPath as arg for CreateElement call!!!
308 return false;
309 }
310 return true;
311 }
312#endif
313}
314
315namespace {
316 struct DocRep_;
317 DocRep_* GetWrapperDoc_ (xmlDoc* d);
318#if qStroika_Foundation_Debug_AssertionsChecked
319 weak_ptr<Document::IRep> DocumentOf_ (xmlNode* n);
320#endif
321 DocRep_* GetWrapperDoc_ (xmlNode* n)
322 {
323 RequireNotNull (n);
324 RequireNotNull (n->doc);
325 return GetWrapperDoc_ (n->doc);
326 }
327 xmlNs* GetSharedReUsableXMLNSParentNamespace_ (xmlDoc* d);
328 xmlNs* GetSharedReUsableXMLNSParentNamespace_ (xmlNode* n)
329 {
330 RequireNotNull (n);
331 RequireNotNull (n->doc);
332 return GetSharedReUsableXMLNSParentNamespace_ (n->doc);
333 }
334}
335
336namespace {
337 Node::Ptr WrapLibXML2NodeInStroikaNode_ (xmlNode* n);
338}
339
340namespace {
341 struct NodeRep_ : ILibXML2NodeRep, Memory::UseBlockAllocationIfAppropriate<NodeRep_> {
342 public:
343 NodeRep_ (xmlNode* n)
344 : fNode_{n}
346 , fOwningDoc_{DocumentOf_ (n)}
347#endif
348 {
349 RequireNotNull (n);
350 }
351#if qStroika_Foundation_Debug_AssertionsChecked
352 ~NodeRep_ ()
353 {
354 // a node is an internal pointer into its document (Node::Ptr): whoever holds it must hold the Document::Ptr longer -
355 // so any use of this node after its document went would have read freed memory. Here catches every such use, but only
356 // once it is too late: the same Require goes at any use worth the cost of checking too
357 Require (not fOwningDoc_.expired ());
358 }
359#endif
360 virtual const Providers::IDOMProvider* GetProvider () const override
361 {
362 return &Providers::LibXML2::kDefaultProvider;
363 }
364 virtual bool Equals (const IRep* rhs) const override
365 {
366 RequireNotNull (fNode_);
367 RequireNotNull (rhs);
368 return fNode_ == dynamic_cast<const NodeRep_*> (rhs)->fNode_;
369 }
370 virtual Node::Type GetNodeType () const override
371 {
372 AssertNotNull (fNode_);
373 switch (fNode_->type) {
374 case XML_ELEMENT_NODE:
375 return Node::eElementNT;
376 case XML_ATTRIBUTE_NODE:
377 return Node::eAttributeNT;
378 case XML_TEXT_NODE:
379 return Node::eTextNT;
380 case XML_COMMENT_NODE:
381 return Node::eCommentNT;
382 default:
383 return Node::eOtherNT;
384 }
385 }
386 virtual NameWithNamespace GetName () const override
387 {
388 AssertNotNull (fNode_);
389 Require (GetNodeType () == Node::eElementNT or GetNodeType () == Node::eAttributeNT);
390 switch (fNode_->type) {
391 case XML_ATTRIBUTE_NODE:
392 case XML_ELEMENT_NODE: {
393 const xmlChar* ns = fNode_->ns == nullptr ? nullptr : fNode_->ns->href;
394 return NameWithNamespace{ns == nullptr ? optional<URI>{} : URI{libXMLString2String (ns)}, libXMLString2String (fNode_->name)};
395 }
396 default:
398 return NameWithNamespace{""sv};
399 }
400 }
401 virtual void SetName (const NameWithNamespace& name) override
402 {
403 AssertNotNull (fNode_);
404#if qStroika_Foundation_Debug_AssertionsChecked
405 Require (ValidNewNodeName_ (name.fName));
406#endif
407 if (name.fNamespace) {
408 AssertNotReached (); // pretty sure this is wrong...
409 // see genNS2Use_
410 // see SetAttribtues - simple now...
411 }
412 xmlNodeSetName (fNode_, BAD_CAST name.fName.AsUTF8 ().c_str ());
413 }
414 virtual String GetValue () const override
415 {
416 AssertNotNull (fNode_);
417 auto r = xmlNodeGetContent (fNode_);
418 [[maybe_unused]] auto&& cleanup = Execution::Finally ([&] () noexcept { xmlFree (r); });
419 return libXMLString2String (r);
420 }
421 virtual void SetValue (const String& v) override
422 {
423 AssertNotNull (fNode_);
424 // @todo could optimize and avoid xmlEncodeSpecialChars for most cases, by scanning for &<> etc... Maybe imporve logic in WriterUtils.h! - so can use that
425 bool mustEncode = true;
426 if (mustEncode) {
427 xmlChar* p = xmlEncodeSpecialChars (fNode_->doc, BAD_CAST v.AsUTF8 ().c_str ());
428 [[maybe_unused]] auto&& cleanup = Execution::Finally ([&] () noexcept { xmlFree (p); });
429 ::xmlNodeSetContent (fNode_, p);
430 }
431 else {
432 ::xmlNodeSetContent (fNode_, BAD_CAST v.AsUTF8 ().c_str ());
433 }
434 }
435 virtual void DeleteNode () override
436 {
437 RequireNotNull (fNode_);
438 ::xmlUnlinkNode (fNode_);
439 ::xmlFreeNode (fNode_);
440 fNode_ = nullptr;
441 }
442 virtual Node::Ptr GetParentNode () const override
443 {
444 RequireNotNull (fNode_);
445 if (fNode_->parent == nullptr) {
446 return Node::Ptr{nullptr};
447 }
448 return WrapLibXML2NodeInStroikaNode_ (fNode_->parent);
449 }
450 virtual void Write (const Streams::OutputStream::Ptr<byte>& to, const SerializationOptions& options) const override
451 {
452 xmlBufferPtr xmlBuf = xmlBufferCreate ();
453 [[maybe_unused]] auto&& cleanup = Execution::Finally ([&] () noexcept { ::xmlBufferFree (xmlBuf); });
454 if (int dumpRes = ::xmlNodeDump (xmlBuf, fNode_->doc, fNode_, 0, options.fPrettyPrint); dumpRes == -1) {
455 Execution::Throw (Execution::Exception<runtime_error>{"failed dumping node to text"sv});
456 }
457 const xmlChar* t = xmlBufferContent (xmlBuf);
458 AssertNotNull (t);
459 to.Write (span{reinterpret_cast<const byte*> (t), static_cast<size_t> (::strlen (reinterpret_cast<const char*> (t)))});
460 }
461 virtual xmlNode* GetInternalTRep () override
462 {
463 return fNode_;
464 }
465 /*
466 * For an ATTRIBUTE, which is in no namespace at all unless it carries a prefix - a default namespace declaration
467 * applies to elements only (XML Namespaces, section 6.2). So never the prefixless declaration that
468 * xmlSearchNsByHref hands back first: find a PREFIXED one for that href, or declare one.
469 */
470 static xmlNsPtr genPrefixedNS2Use_ (xmlNode* n, const URI& ns)
471 {
472 const string href = ns.As<String> (kUseURIEncodingFlag_).AsUTF8<string> ();
473 for (xmlNode* i = n; i != nullptr and i->type == XML_ELEMENT_NODE; i = i->parent) {
474 for (xmlNs* d = i->nsDef; d != nullptr; d = d->next) {
475 if (d->prefix != nullptr and ::xmlStrEqual (d->href, BAD_CAST href.c_str ())) {
476 return d;
477 }
478 }
479 }
480 string prefix2Try{"a"};
481 xmlNsPtr ns2Use = nullptr;
482 while (ns2Use == nullptr) {
483 ns2Use = ::xmlNewNs (n, BAD_CAST href.c_str (), BAD_CAST prefix2Try.c_str ());
484 if (ns2Use == nullptr) {
485 ++prefix2Try[0]; // that prefix is taken here, by another namespace - as genNS2Use_ has it
486 }
487 }
488 return ns2Use;
489 }
490 /*
491 * The namespace a new element goes in: kNoNamespace none at all, kInheritNamespace (which is what a name built
492 * without one has) its parent's, and otherwise the one it names.
493 */
494 static xmlNsPtr elementNS2Use_ (xmlNode* parent, const NameWithNamespace& eltName)
495 {
496 if (eltName.fNamespace == kNoNamespace) {
497 return nullptr;
498 }
499 return eltName.fNamespace ? genNS2Use_ (parent, *eltName.fNamespace) : parent->ns;
500 }
501 /*
502 * An element in no namespace, under one that declared a default, must SAY so - xmlns="" - or it reads back in that
503 * default. libxml2 writes what the node carries and reconciles nothing, so say it here.
504 */
505 static void undeclareDefaultNSIfInScope_ (xmlNode* newNode)
506 {
507 AssertNotNull (newNode);
508 if (newNode->ns == nullptr) {
509 xmlNs* inScope = ::xmlSearchNs (newNode->doc, newNode->parent, nullptr);
510 if (inScope != nullptr and inScope->href != nullptr and *inScope->href != '\0') {
511 ::xmlSetNsProp (newNode, GetSharedReUsableXMLNSParentNamespace_ (newNode), BAD_CAST "xmlns", BAD_CAST "");
512 }
513 }
514 }
515 static xmlNsPtr genNS2Use_ (xmlNode* n, const URI& ns)
516 {
517 xmlNsPtr ns2Use = xmlSearchNsByHref (n->doc, n, BAD_CAST ns.As<String> (kUseURIEncodingFlag_).AsUTF8 ().c_str ());
518 string prefix2Try{"a"};
519 while (ns2Use == nullptr) {
520 // Need to hang the namespace declaration someplace? Could do it just on this element (xmlNewNs)
521 // Or on the root doc (xmlNewGlobalNs).
522 // For now - do on DOC, so we end up with a more terse overall document.
523 // Also - sadly - must cons up SOME prefix, which doesn't conflict. No good way I can see todo that, so do a bad way.
524 // OK - can do still manually using docs root elt - maybe - but do this way for now... cuz xmlNewGlobalNs deprecated
525 ns2Use = xmlNewNs (n, BAD_CAST ns.As<String> (kUseURIEncodingFlag_).AsUTF8 ().c_str (), BAD_CAST prefix2Try.c_str ());
526 if (ns2Use == nullptr) {
527 ++prefix2Try[0]; // if 'a' didn't work, try 'b' // @todo this could use better error handling, but pragmatically probably OK
528 }
529 }
530 return ns2Use;
531 }
532 // not ref counted: freed with its document, which whoever holds this must keep (Node::Ptr) - checked in the DTOR
533 xmlNode* fNode_;
534#if qStroika_Foundation_Debug_AssertionsChecked
535 weak_ptr<Document::IRep> fOwningDoc_;
536#endif
537 };
538}
539
540namespace {
541 DISABLE_COMPILER_MSC_WARNING_START (4250) // inherits via dominance warning
542 struct ElementRep_ : Element::IRep, Memory::InheritAndUseBlockAllocationIfAppropriate<ElementRep_, NodeRep_> {
544 ElementRep_ (xmlNode* n)
545 : inherited{n}
546 {
547 RequireNotNull (n);
548 Require (n->type == XML_ELEMENT_NODE);
549 }
550 virtual Node::Type GetNodeType () const override
551 {
552 AssertNotNull (fNode_);
553 Assert (fNode_->type == XML_ELEMENT_NODE);
554 return Node::eElementNT;
555 }
556 virtual optional<String> GetAttribute (const NameWithNamespace& attrName) const override
557 {
558 auto r = attrName.fNamespace ? ::xmlGetNsProp (fNode_, BAD_CAST attrName.fName.AsUTF8 ().c_str (),
559 BAD_CAST attrName.fNamespace->As<String> (kUseURIEncodingFlag_).AsUTF8 ().c_str ())
560 : ::xmlGetProp (fNode_, BAD_CAST attrName.fName.AsUTF8 ().c_str ());
561 if (r == nullptr) {
562 return nullopt;
563 }
564 [[maybe_unused]] auto&& cleanup = Execution::Finally ([&] () noexcept { xmlFree (r); });
565 return ::libXMLString2String (r);
566 }
567 virtual void SetAttribute (const NameWithNamespace& attrName, const optional<String>& v) override
568 {
569 RequireNotNull (fNode_);
570 Require (GetNodeType () == Node::eElementNT);
571 // any attribute IN the xmlns namespace is a namespace declaration - xmlns itself, or xmlns:PREFIX - and the one
572 // namespace no prefix may be bound to, so never the prefixing below
573 if (attrName.fNamespace == kXMLNS.fNamespace and attrName.fName != kXMLNS.fName) {
574 /*
575 * xmlns:PREFIX - a prefix declaration, which to libxml2 is not an attribute at all but an xmlNs hung on
576 * the node, and so is made with xmlNewNs. Written as an attribute instead it comes out named for the
577 * prefix alone (s="..."), declaring nothing. Nothing undeclares a prefix, so a nullopt is no request.
578 */
579 if (v) {
580 ::xmlNewNs (fNode_, BAD_CAST v->AsUTF8<string> ().c_str (), BAD_CAST attrName.fName.AsUTF8<string> ().c_str ());
581 }
582 }
583 else if (attrName.fNamespace == kXMLNS.fNamespace) {
584 /*
585 * Queer, but libxml2 appears to require this attribute have the given namespace (fine) - but not provide a usable xmlNs object.
586 * So we must create it on the document, and free it when we free the document.
587 *
588 * \see https://github.com/SophistSolutions/Stroika/issues/1133 (STK-1001)
589 *
590 * A nullopt here is NOT the delete below: xmlns="" is how XML undeclares a default namespace, which is
591 * just what Element::Ptr::SetDefaultNamespace (nullopt) asks for.
592 */
593 ::xmlSetNsProp (fNode_, GetSharedReUsableXMLNSParentNamespace_ (fNode_), BAD_CAST attrName.fName.AsUTF8 ().c_str (),
594 v == nullopt ? nullptr : (BAD_CAST v->AsUTF8 ().c_str ()));
595 }
596 else if (v == nullopt) {
597 /*
598 * nullopt deletes the attribute. xmlSetProp with a nullptr value does not: it leaves an empty attribute
599 * behind, and creates one for a name that was never there. And genNS2Use_ would DECLARE a namespace to
600 * delete through, so search for one instead - where none is declared, no attribute is in it.
601 */
602 if (attrName.fNamespace) {
603 if (xmlNs* ns = ::xmlSearchNsByHref (fNode_->doc, fNode_,
604 BAD_CAST attrName.fNamespace->As<String> (kUseURIEncodingFlag_).AsUTF8 ().c_str ())) {
605 ::xmlUnsetNsProp (fNode_, ns, BAD_CAST attrName.fName.AsUTF8 ().c_str ());
606 }
607 }
608 else {
609 ::xmlUnsetProp (fNode_, BAD_CAST attrName.fName.AsUTF8 ().c_str ());
610 }
611 }
612 else if (attrName.fNamespace) {
613 // Lookup the argument ns and either add it to this node or use the existing one - a PREFIXED one, @see genPrefixedNS2Use_
614 ::xmlSetNsProp (fNode_, genPrefixedNS2Use_ (fNode_, *attrName.fNamespace), BAD_CAST attrName.fName.AsUTF8 ().c_str (),
615 BAD_CAST v->AsUTF8 ().c_str ());
616 }
617 else {
618 ::xmlSetProp (fNode_, BAD_CAST attrName.fName.AsUTF8 ().c_str (), BAD_CAST v->AsUTF8 ().c_str ());
619 }
620 }
621 virtual Element::Ptr InsertElement (const NameWithNamespace& eltName, const Element::Ptr& afterNode) override
622 {
623#if qStroika_Foundation_Debug_AssertionsChecked
624 Require (ValidNewNodeName_ (eltName.fName));
625#endif
626 Require (afterNode == nullptr or this->GetChildren ().Contains (afterNode));
627 // when adding a child, if no NS specified, copy parents
628 xmlNs* useNS = elementNS2Use_ (fNode_, eltName);
629 xmlNode* newNode = ::xmlNewNode (useNS, BAD_CAST eltName.fName.AsUTF8 ().c_str ());
630 NodeRep_* afterNodeRep = afterNode == nullptr ? nullptr : dynamic_cast<NodeRep_*> (afterNode.GetRep ().get ());
631 if (afterNodeRep == nullptr) {
632 // unfortunately complicated - no prepend api (just append). Can say xmlAddPrevSibling for first child though which amounts
633 // to same thing (unless there is no first child)
634 if (fNode_->children == nullptr) {
635 ::xmlAddChild (fNode_, newNode); // nothing to go before, so append=prepend
636 }
637 else {
638 ::xmlAddPrevSibling (fNode_->children, newNode);
639 }
640 }
641 else {
642 xmlAddNextSibling (afterNodeRep->fNode_, newNode);
643 }
644 undeclareDefaultNSIfInScope_ (newNode);
645 return WrapLibXML2NodeInStroikaNode_ (newNode);
646 }
647 virtual Element::Ptr AppendElement (const NameWithNamespace& eltName) override
648 {
649#if qStroika_Foundation_Debug_AssertionsChecked
650 Require (ValidNewNodeName_ (eltName.fName));
651#endif
652 // when adding a child, if no NS specified, copy parents
653 xmlNs* useNS = elementNS2Use_ (fNode_, eltName);
654 xmlNode* newNode = xmlNewNode (useNS, BAD_CAST eltName.fName.AsUTF8 ().c_str ());
655 ::xmlAddChild (fNode_, newNode);
656 undeclareDefaultNSIfInScope_ (newNode);
657 return WrapLibXML2NodeInStroikaNode_ (newNode);
658 }
659 /*
660 * A deep COPY into this document - xmlDocCopyNode, never xmlAddChild of the original, which would move it out of
661 * the document it belongs to. xmlReconciliateNs then declares, on the copy, whatever namespaces it referred to
662 * through its old document's declarations - without which a grafted subtree points at an xmlNs that is not there.
663 */
664 xmlNode* CloneIntoThisDocument_ (const Node::Ptr& cloneFrom)
665 {
666 RequireNotNull (cloneFrom);
667 const NodeRep_* fromRep = dynamic_cast<const NodeRep_*> (cloneFrom.GetRep ().get ());
668 Require (fromRep != nullptr); // the caller's contract: both documents are the same provider
669 xmlNode* copy = ::xmlDocCopyNode (fromRep->fNode_, fNode_->doc, 1 /*recursive, with attributes*/);
670 Execution::ThrowIfNull (copy);
671 return copy;
672 }
673 virtual Element::Ptr AppendImportedNode (const Node::Ptr& cloneFrom) override
674 {
675 xmlNode* copy = CloneIntoThisDocument_ (cloneFrom);
676 ::xmlAddChild (fNode_, copy);
677 ::xmlReconciliateNs (fNode_->doc, copy);
678 return WrapLibXML2NodeInStroikaNode_ (copy);
679 }
680 virtual Element::Ptr InsertImportedNode (const Node::Ptr& cloneFrom, const Element::Ptr& afterNode) override
681 {
682 Require (afterNode == nullptr or this->GetChildren ().Contains (afterNode));
683 xmlNode* copy = CloneIntoThisDocument_ (cloneFrom);
684 NodeRep_* afterNodeRep = afterNode == nullptr ? nullptr : dynamic_cast<NodeRep_*> (afterNode.GetRep ().get ());
685 if (afterNodeRep == nullptr) {
686 if (fNode_->children == nullptr) {
687 ::xmlAddChild (fNode_, copy); // nothing to go before, so first is last
688 }
689 else {
690 ::xmlAddPrevSibling (fNode_->children, copy);
691 }
692 }
693 else {
694 ::xmlAddNextSibling (afterNodeRep->fNode_, copy);
695 }
696 ::xmlReconciliateNs (fNode_->doc, copy);
697 return WrapLibXML2NodeInStroikaNode_ (copy);
698 }
699 virtual Iterable<Node::Ptr> GetChildren () const override
700 {
701 RequireNotNull (fNode_);
702 // No reference counting possible here because these notes - i THINK - are owned by document, and the linked list not
703 // reference counted (elts) - so just count on no changes during iteration
704 return Traversal::CreateGenerator<Node::Ptr> ([curChild = fNode_->children] () mutable -> optional<Node::Ptr> {
705 if (curChild == nullptr) {
706 return optional<Node::Ptr>{};
707 }
708 Node::Ptr r = WrapLibXML2NodeInStroikaNode_ (curChild);
709 curChild = curChild->next;
710 return r;
711 });
712 }
713 struct XPathLookupHelper_ final {
714 xmlXPathContext* fCtx{nullptr};
715 xmlXPathObject* fResultNodeList{nullptr};
716 XPathLookupHelper_ () = delete;
717 XPathLookupHelper_ (const XPathLookupHelper_&) = delete;
718 XPathLookupHelper_ (xmlDoc* doc, xmlNode* contextNode, const XPath::Expression& e)
719 {
720 // based on code from http://www.xmlsoft.org/examples/xpath1.c
721 RequireNotNull (doc);
722 fCtx = xmlXPathNewContext (doc);
723 Execution::ThrowIfNull (fCtx);
724 fCtx->error = [] ([[maybe_unused]] void* userData, [[maybe_unused]] const xmlError* error) {
725 // default function prints to console; we capture 'lastError' later so no need to do anything here.
726 };
727 try {
728 auto namespaceDefs = e.GetOptions ().fNamespaces;
729 if (namespaceDefs.GetDefaultNamespace ()) {
730 // According to https://gitlab.gnome.org/GNOME/libxml2/-/issues/585, this is XPath 2 feature NYI in libxml2
731 Execution::Throw (XPath::XPathExpressionNotSupported::kThe);
732 }
733 for (Common::KeyValuePair ni : namespaceDefs.GetPrefixedNamespaces ()) {
734 xmlXPathRegisterNs (fCtx, BAD_CAST ni.fKey.AsUTF8 ().c_str (),
735 BAD_CAST ni.fValue.As<String> (kUseURIEncodingFlag_).AsUTF8 ().c_str ());
736 }
737 fCtx->node = contextNode;
738 fResultNodeList = xmlXPathEvalExpression (BAD_CAST e.GetExpression ().AsUTF8 ().c_str (), fCtx);
739 if (fCtx->lastError.level != XML_ERR_NONE and fCtx->lastError.level != XML_ERR_WARNING) {
740 // lookup domain in xmlErrorDomain, and lastError.code in xmlParserErrors
741 Execution::ThrowIfNull (fResultNodeList, Execution::Exception<runtime_error>{"Error parsing xpath {}: (domain {}, code {})"_f(
742 e, fCtx->lastError.domain, fCtx->lastError.code)});
743 }
744 Execution::ThrowIfNull (fResultNodeList);
745 }
746 catch (...) {
747 if (fResultNodeList != nullptr) {
748 xmlXPathFreeObject (fResultNodeList);
749 fResultNodeList = nullptr;
750 }
751 if (fCtx != nullptr) {
752 xmlXPathFreeContext (fCtx);
753 fCtx = nullptr;
754 }
755 Execution::ReThrow ();
756 }
757 }
758 ~XPathLookupHelper_ ()
759 {
760 AssertNotNull (fResultNodeList);
761 xmlXPathFreeObject (fResultNodeList);
762 AssertNotNull (fCtx);
763 xmlXPathFreeContext (fCtx);
764 }
765 static optional<XPath::Result> ToResult (xmlNode* n)
766 {
767 if (n == nullptr) [[unlikely]] {
768 return nullopt;
769 }
770 // for now only support elements/attributes...
771 switch (n->type) {
772 case XML_ATTRIBUTE_NODE:
773 case XML_ELEMENT_NODE: {
774 return WrapLibXML2NodeInStroikaNode_ (n);
775 }
776 }
777 return nullopt;
778 }
779 };
780 virtual optional<XPath::Result> LookupOne (const XPath::Expression& e) override
781 {
782 RequireNotNull (fNode_);
783 AssertNotNull (fNode_->doc);
784 XPathLookupHelper_ helper{fNode_->doc, fNode_, e};
785 xmlNodeSet* resultSet = helper.fResultNodeList->nodesetval;
786 size_t size = (resultSet) ? resultSet->nodeNr : 0;
787 if (size > 0) {
788 return XPathLookupHelper_::ToResult (resultSet->nodeTab[0]);
789 }
790 return nullopt;
791 }
792 virtual Traversal::Iterable<XPath::Result> Lookup (const XPath::Expression& e) override
793 {
794 // Could use generator, but little point since libxml2 has already done all the work inside
795 // xmlXPathEvalExpression, and we'd just save the wrapping in Stroika Node::Rep object/shared_ptr
796 // For now, KISS
797 // So essentially treat as if e.GetOptions().fSnapshot == true
798 // @todo - see if anything needed to support fOrdering???
799 RequireNotNull (fNode_);
800 AssertNotNull (fNode_->doc);
801 XPathLookupHelper_ helper{fNode_->doc, fNode_, e};
802 xmlNodeSet* resultSet = helper.fResultNodeList->nodesetval;
803 size_t size = (resultSet) ? resultSet->nodeNr : 0;
805 for (size_t i = 0; i < size; ++i) {
806 r += Memory::ValueOf (XPathLookupHelper_::ToResult (resultSet->nodeTab[i]));
807 }
808 return r;
809 }
810 };
811 DISABLE_COMPILER_MSC_WARNING_END (4250) // inherits via dominance warning
812}
813
814namespace {
815 Node::Ptr WrapLibXML2NodeInStroikaNode_ (xmlNode* n)
816 {
817 RequireNotNull (n);
818 if (n->type == XML_ELEMENT_NODE) [[likely]] {
819 return Node::Ptr{MakeSharedPtr<ElementRep_> (n)};
820 }
821 else {
822 return Node::Ptr{MakeSharedPtr<NodeRep_> (n)};
823 }
824 }
825}
826
827namespace {
828 struct MyLibXML2StructuredErrGrabber_ final {
829 xmlParserCtxtPtr fCtx;
830 // the first error, as thrown - so rethrown whole: a throw of *pointer-to-base would slice it
831 exception_ptr fCapturedException;
832
833 MyLibXML2StructuredErrGrabber_ (xmlParserCtxtPtr ctx)
834 : fCtx{ctx}
835 {
836 xmlCtxtSetErrorHandler (ctx, xmlStructuredErrorFunc_, this);
837 }
838 ~MyLibXML2StructuredErrGrabber_ ()
839 {
840 xmlCtxtSetErrorHandler (fCtx, nullptr, nullptr);
841 }
842 MyLibXML2StructuredErrGrabber_& operator= (const MyLibXML2StructuredErrGrabber_&) = delete;
843
844 void ThrowIf ()
845 {
846 if (fCapturedException != nullptr) {
847 Execution::ReThrow (fCapturedException);
848 }
849 }
850
851 private:
852 static void xmlStructuredErrorFunc_ (void* userData, const xmlError* error)
853 {
854 // nice to throw but this is 'C' land, so probably not safe
855 MyLibXML2StructuredErrGrabber_* useThis = reinterpret_cast<MyLibXML2StructuredErrGrabber_*> (userData);
856 // save first error
857 if (useThis->fCapturedException == nullptr) {
858 switch (error->level) {
859 case XML_ERR_NONE:
861 break;
862 case XML_ERR_WARNING:
863 DbgTrace ("libxml2 (xmlStructuredErrorFunc_): Ignore warnings for now: {}"_f, String::FromUTF8 (error->message));
864 break;
865 case XML_ERR_ERROR:
866 case XML_ERR_FATAL:
867 DbgTrace ("libxml2 (xmlStructuredErrorFunc_): Capturing Error {}"_f, String::FromUTF8 (error->message));
868 // thrown and caught at once - never through libxml2's C - so Throw stamps it with the current Activities
869 try {
870 Execution::Throw (DataExchange::BadFormatException{
871 "Failure Parsing XML: {}, line {}"_f(String::FromUTF8 (error->message), error->line),
872 static_cast<unsigned int> (error->line), nullopt, nullopt});
873 }
874 catch (...) {
875 useThis->fCapturedException = current_exception ();
876 }
877 break;
878 }
879 }
880 };
881 };
882}
883
884namespace {
885 // enable_shared_from_this is only to give each node a weak_ptr to its document (DocumentOf_, from the DocRep_* libxml2 keeps
886 // in _private - a weak_ptr can be had from a raw pointer no other way): its DTOR check, so nothing without assertions
887 struct DocRep_ final : ILibXML2DocRep
888#if qStroika_Foundation_Debug_AssertionsChecked
889 ,
890 enable_shared_from_this<DocRep_>
891#endif
892 {
893#if qStroika_Foundation_DataExchange_XML_DebugMemoryAllocations
894 static inline atomic<unsigned int> sLiveCnt{0};
895#endif
896 public:
897 DocRep_ (const shared_ptr<const void>& library, const Streams::InputStream::Ptr<byte>& in)
898 : fLibrary_{library}
899 {
900 if (in == nullptr) {
901 fLibRep_ = xmlNewDoc (BAD_CAST "1.0");
902 fLibRep_->standalone = true;
903 }
904 else {
905 xmlParserCtxtPtr ctxt = xmlCreatePushParserCtxt (nullptr, nullptr, nullptr, 0, "in-stream.xml" /*filename*/);
906 Execution::ThrowIfNull (ctxt);
907 // failing, the document it began is not kept: freed too (xmlFreeParserCtxt does not free its myDoc)
908 [[maybe_unused]] auto&& cleanup = Execution::Finally ([&] () noexcept {
909 if (fLibRep_ == nullptr and ctxt->myDoc != nullptr) {
910 xmlFreeDoc (ctxt->myDoc);
911 }
912 xmlFreeParserCtxt (ctxt);
913 });
914 MyLibXML2StructuredErrGrabber_ errCatcher{ctxt};
915 Stroika_ATTRIBUTE_INDETERMINATE byte buf[1024];
916 while (auto n = in.ReadBlocking (span{buf}).size ()) {
917 if (xmlParseChunk (ctxt, reinterpret_cast<char*> (buf), static_cast<int> (n), 0)) {
918 AssertNotNull (errCatcher.fCapturedException); // double check I understood API properly - and error handler getting called
919 }
920 errCatcher.ThrowIf ();
921 }
922 xmlParseChunk (ctxt, nullptr, 0, 1); // indicate the parsing is finished
923 errCatcher.ThrowIf ();
924 if (not ctxt->wellFormed) {
925 Execution::Throw (BadFormatException{"not well formed"sv}); // get good error message and throw that BadFormatException
926 }
927 fLibRep_ = ctxt->myDoc;
928 }
929 /*
930 * From https://opensource.apple.com/source/libxml2/libxml2-7/libxml2/doc/html/libxml-tree.html
931 *
932 * void * _private : For user data, libxml won't touch it (sometimes it says 'application data' - which is a bit less clear)
933 */
934 fLibRep_->_private = this;
935#if qStroika_Foundation_DataExchange_XML_DebugMemoryAllocations
936 ++sLiveCnt;
937#endif
938 }
939 DocRep_ (const DocRep_& from)
940 : fLibrary_{from.fLibrary_}
941 {
942 fLibRep_ = xmlCopyDoc (from.fLibRep_, 1); // unclear if this does the right thing with the xmlns???? if any pointers to it??? --LGP 2024-02-04
943 fLibRep_->_private = this;
944#if qStroika_Foundation_DataExchange_XML_DebugMemoryAllocations
945 ++sLiveCnt;
946#endif
947 }
948 DocRep_ (DocRep_&&) = delete;
949 DocRep_& operator= (DocRep_&) = delete;
950 ~DocRep_ ()
951 {
952 AssertNotNull (fLibRep_);
953 Assert (fLibRep_->_private == this);
954 xmlFreeDoc (fLibRep_);
955 for (auto i : fNSs2Free_) {
956 xmlFreeNs (i);
957 }
958#if qStroika_Foundation_DataExchange_XML_DebugMemoryAllocations
959 Assert (sLiveCnt > 0);
960 --sLiveCnt;
961#endif
962 }
963 virtual xmlDoc* GetLibXMLDocRep () override
964 {
965 return fLibRep_;
966 }
967 virtual const Providers::IDOMProvider* GetProvider () const override
968 {
969 return &Providers::LibXML2::kDefaultProvider;
970 }
971 virtual bool GetStandalone () const override
972 {
973 return !!fLibRep_->standalone;
974 }
975 virtual void SetStandalone (bool standalone) override
976 {
977 fLibRep_->standalone = standalone;
978 }
979 virtual Element::Ptr ReplaceRootElement (const NameWithNamespace& newEltName, const optional<String>& namespacePrefix) override
980 {
981 // This API is very confusing. I could find no examples online, or clear documentation on how to handle
982 // create a prefixed namespace and default namespace.
983 // I only came upon this series of hacks after looking carefully at the code and alot of experimenting...
984 // --LGP 2024-02-29
985 xmlNsPtr ns{nullptr};
986 if (newEltName.fNamespace) {
987 const string href = newEltName.fNamespace->As<String> (kUseURIEncodingFlag_).AsUTF8<string> ();
988 // a prefix declares xmlns:PREFIX and the element is written with it; none declares the default namespace
989 ns = namespacePrefix == nullopt
990 ? xmlNewNs (nullptr, BAD_CAST href.c_str (), nullptr)
991 : xmlNewNs (nullptr, BAD_CAST href.c_str (), BAD_CAST namespacePrefix->AsUTF8<string> ().c_str ());
992 }
993 xmlNodePtr n = xmlNewDocNode (fLibRep_, ns, BAD_CAST newEltName.fName.AsUTF8 ().c_str (), nullptr);
994 Assert (n->nsDef == nullptr);
995 if (newEltName.fNamespace) {
996 n->nsDef = ns; // UGH - the declaration must hang on the element, prefixed or not, or nothing declares it
997 }
998 // the root it replaces comes back unlinked - so no longer freed with the document - and goes, as Replace's does
999 if (xmlNodePtr oldRoot = xmlDocSetRootElement (fLibRep_, n)) {
1000 xmlFreeNode (oldRoot);
1001 }
1002 auto r = WrapLibXML2NodeInStroikaNode_ (n);
1003 Ensure (r.GetName () == newEltName);
1004 return r;
1005 }
1006 virtual void Write (const Streams::OutputStream::Ptr<byte>& to, const SerializationOptions& options) const override
1007 {
1008 TraceContextBumper ctx{"LibXML2::Doc::Write"};
1009 AssertNotNull (fLibRep_);
1010 xmlBufferPtr xmlBuf = xmlBufferCreate ();
1011 [[maybe_unused]] auto&& cleanup1 = Execution::Finally ([&] () noexcept { xmlBufferFree (xmlBuf); });
1012 constexpr char kTxtEncoding_[] = "UTF-8";
1013 int useOptions = XML_SAVE_AS_XML;
1014 if (options.fPrettyPrint) {
1015 useOptions |= XML_SAVE_FORMAT | XML_SAVE_WSNONSIG;
1016 }
1017 // use xmlSaveToBuffer instead of xmlDocDumpFormatMemoryEnc () since that has options to control XML_SAVE_NO_EMPTY which changed in libxml2 2.13.1
1018 xmlSaveCtxtPtr saveCtx = xmlSaveToBuffer (xmlBuf, kTxtEncoding_, useOptions);
1019 [[maybe_unused]] auto&& cleanup2 = Execution::Finally ([&] () noexcept { xmlSaveClose (saveCtx); });
1020#if LIBXML_VERSION >= 21400
1021 if (options.fPrettyPrint and options.fIndent) {
1022 (void)xmlSaveSetIndentString (saveCtx, string (*options.fIndent, ' ').c_str ()); // one it rejects (0, over 60) leaves its default
1023 }
1024#endif
1025 // could check for > 0 but bug in incomplete iml in current impl as libxml2 2.13.2
1026 if (xmlSaveDoc (saveCtx, fLibRep_) >= 0 and xmlSaveFlush (saveCtx) >= 0) {
1027 to.Write (span{reinterpret_cast<const byte*> (xmlBufferContent (xmlBuf)), static_cast<size_t> (xmlBufferLength (xmlBuf))});
1028 }
1029 else {
1030 Execution::Throw (Execution::Exception<runtime_error>{"failed dumping documented to text"});
1031 return;
1032 }
1033 }
1034 virtual Iterable<Node::Ptr> GetChildren () const override
1035 {
1036 AssertExternallySynchronizedChecker::ReadContext declareContext{fThisAssertExternallySynchronized_};
1037 return Traversal::CreateGenerator<Node::Ptr> ([curChild = fLibRep_->children] () mutable -> optional<Node::Ptr> {
1038 if (curChild == nullptr) {
1039 return optional<Node::Ptr>{};
1040 }
1041 Node::Ptr r = WrapLibXML2NodeInStroikaNode_ (curChild);
1042 curChild = curChild->next;
1043 return r;
1044 });
1045 }
1046 virtual void Validate (const Schema::Ptr& schema) const override
1047 {
1048 TraceContextBumper ctx{"LibXML2::Doc::Validate"};
1049 RequireNotNull (schema);
1050 Require (schema.GetRep ()->GetProvider () == &XML::Providers::LibXML2::kDefaultProvider);
1051 xmlSchema* libxml2Schema = dynamic_pointer_cast<ILibXML2SchemaRep> (schema.GetRep ())->GetSchemaLibRep ();
1052 RequireNotNull (libxml2Schema);
1053 xmlSchemaValidCtxtPtr validateCtx = xmlSchemaNewValidCtxt (libxml2Schema);
1054 [[maybe_unused]] auto&& cleanup = Execution::Finally ([&] () noexcept { xmlSchemaFreeValidCtxt (validateCtx); });
1055 SchemaErrors_ errors{.fWhatFailed = "Failed schema validation"_k};
1056 xmlSchemaSetValidStructuredErrors (validateCtx, &SchemaErrors_::Handler, &errors);
1057 if (xmlSchemaValidateDoc (validateCtx, fLibRep_) != 0) {
1058 Assert (not errors.fFirstError.empty ()); // guessing we only get validation error if error callback called?
1059 Execution::Throw (errors.AsException ());
1060 }
1061 }
1062 shared_ptr<const void> fLibrary_; // first, so destroyed last: libxml2 lasts while anything of it does
1063 xmlDoc* fLibRep_{nullptr};
1064 xmlNs* fXmlnsNamespace2Use{nullptr}; // some APIs require this existing, but not sure where to put it??? and dont want to create a bunch of them... --LGP 2024-02-04
1065 list<xmlNsPtr> fNSs2Free_; // There probably is a better way with limxml2, but I cannot see how to avoid leaking these namespaces without this
1067 };
1068 DocRep_* GetWrapperDoc_ (xmlDoc* d)
1069 {
1070 RequireNotNull (d);
1071 DocRep_* wrapperDoc = reinterpret_cast<DocRep_*> (d->_private);
1072 Assert (wrapperDoc->fLibRep_ == d); // else grave disorder
1073 return wrapperDoc;
1074 }
1075#if qStroika_Foundation_Debug_AssertionsChecked
1076 // the document a node belongs to - empty if it has none, or none a shared_ptr owns yet (while one is being built)
1077 weak_ptr<Document::IRep> DocumentOf_ (xmlNode* n)
1078 {
1079 RequireNotNull (n);
1080 if (n->doc == nullptr or n->doc->_private == nullptr) {
1081 return {};
1082 }
1083 return GetWrapperDoc_ (n->doc)->weak_from_this ();
1084 }
1085#endif
1086 xmlNs* GetSharedReUsableXMLNSParentNamespace_ (xmlDoc* d)
1087 {
1088 auto wrapperDoc = GetWrapperDoc_ (d);
1089 if (wrapperDoc->fXmlnsNamespace2Use == nullptr) {
1090 // lazy create, since not always needed
1091 wrapperDoc->fXmlnsNamespace2Use =
1092 xmlNewNs (nullptr, BAD_CAST kXMLNS.fNamespace->As<String> (kUseURIEncodingFlag_).AsUTF8 ().c_str (), nullptr);
1093 wrapperDoc->fNSs2Free_.push_front (wrapperDoc->fXmlnsNamespace2Use);
1094 }
1095 return wrapperDoc->fXmlnsNamespace2Use;
1096 }
1097}
1098
1099/*
1100 ********************************************************************************
1101 ******************* Provider::Xerces::libXMLString2String **********************
1102 ********************************************************************************
1103 */
1104String Providers::LibXML2::libXMLString2String (const xmlChar* s, int len)
1105{
1106 return String::FromUTF8 (span{reinterpret_cast<const char*> (s), static_cast<size_t> (len)});
1107}
1108String Providers::LibXML2::libXMLString2String (const xmlChar* t)
1109{
1110 return String::FromUTF8 (reinterpret_cast<const char*> (t));
1111}
1112
1113/*
1114 ********************************************************************************
1115 ***************** Provider::Xerces::Providers::LibXML2::Provider ***************
1116 ********************************************************************************
1117 */
1118namespace {
1119 // libxml2 itself: the provider, and each schema and document it makes, hold one of these (as a shared_ptr<const void>), so
1120 // xmlCleanupParser runs once the last of them is gone, in whatever order they go
1121 struct Library_ {
1122 Library_ ()
1123 {
1124 LIBXML_TEST_VERSION;
1125 }
1126 Library_ (const Library_&) = delete;
1127 ~Library_ ()
1128 {
1129 TraceContextBumper ctx{"LibXML2::Library_::DTOR"};
1130#if qStroika_Foundation_DataExchange_XML_DebugMemoryAllocations
1131 Assert (SchemaRep_::sLiveCnt == 0); // each holds this, so none can be left
1132 Assert (DocRep_::sLiveCnt == 0); // ""
1133#endif
1134 xmlCleanupParser ();
1135 }
1136 };
1137}
1138
1139Providers::LibXML2::Provider::Provider ()
1140 : fLibrary_{MakeSharedPtr<Library_> ()}
1141{
1142 TraceContextBumper ctx{"LibXML2::Provider::CTOR"};
1143#if qStroika_Foundation_Debug_AssertionsChecked
1144 static unsigned int sNProvidersCreated_{0}; // don't create multiple of these - will lead to confusion
1145 Assert (++sNProvidersCreated_ == 1);
1146#endif
1147}
1148
1149shared_ptr<Schema::IRep> Providers::LibXML2::Provider::SchemaFactory (const InputStream::Ptr<byte>& schemaData, const Resource::ResolverPtr& resolver) const
1150{
1151 return MakeSharedPtr<SchemaRep_> (fLibrary_, schemaData, resolver);
1152}
1153
1154shared_ptr<DOM::Document::IRep> Providers::LibXML2::Provider::DocumentFactory (const Streams::InputStream::Ptr<byte>& in,
1155 const Schema::Ptr& schemaToValidateAgainstWhileReading) const
1156{
1157 auto r = MakeSharedPtr<DocRep_> (fLibrary_, in);
1158 if (schemaToValidateAgainstWhileReading != nullptr) {
1159 r->Validate (schemaToValidateAgainstWhileReading);
1160 }
1161 return r;
1162}
1163
1164void Providers::LibXML2::Provider::SAXParse (const Streams::InputStream::Ptr<byte>& in, StructuredStreamEvents::IConsumer* callback,
1165 const Schema::Ptr& schema) const
1166{
1167 Streams::InputStream::Ptr<byte> useInput = in;
1168 optional<Streams::SeekOffsetType> seek2;
1169 if (schema != nullptr and callback != nullptr) {
1170 // @todo --- at least for now this is needed - cuz we read twice - maybe can fix...
1171 useInput = Streams::ToSeekableInputStream::New (in);
1172 seek2 = useInput.GetOffset ();
1173 }
1174 if (schema != nullptr) {
1175 DocRep_ dr{fLibrary_, useInput};
1176 dr.Validate (schema);
1177 }
1178 if (callback != nullptr) {
1179 SAXReader_ handler{*callback};
1180 xmlParserCtxtPtr ctxt = xmlCreatePushParserCtxt (&handler.flibXMLSaxHndler_, &handler, nullptr, 0, nullptr);
1182 handler.fCtxt_ = ctxt; // so a callback that throws can stop the parse (see SAXReader_::Invoke_)
1183 [[maybe_unused]] auto&& cleanup = Execution::Finally ([&] () noexcept { xmlFreeParserCtxt (ctxt); });
1184 MyLibXML2StructuredErrGrabber_ errCatcher{ctxt};
1185 byte buf[1024];
1186 if (seek2) {
1187 useInput.Seek (*seek2);
1188 }
1189 while (auto n = useInput.ReadBlocking (span{buf}).size ()) {
1190 if (xmlParseChunk (ctxt, reinterpret_cast<char*> (buf), static_cast<int> (n), 0)) {
1191 // a consumer that threw stopped the parse itself, so its exception is the error - not whatever libxml2 made of being stopped
1192 handler.ThrowIfCapturedException ();
1193 AssertNotNull (errCatcher.fCapturedException); // double check I understood API properly - and error handler getting called
1194 }
1195 handler.ThrowIfCapturedException ();
1196 errCatcher.ThrowIf ();
1197 }
1198 xmlParseChunk (ctxt, nullptr, 0, 1);
1199 handler.ThrowIfCapturedException ();
1200 errCatcher.ThrowIf ();
1201 }
1202}
#define AssertNotNull(p)
Definition Assertions.h:336
#define qStroika_Foundation_Debug_AssertionsChecked
The qStroika_Foundation_Debug_AssertionsChecked flag determines if assertions are checked and validat...
Definition Assertions.h:49
#define RequireNotNull(p)
Definition Assertions.h:350
#define AssertNotReached()
Definition Assertions.h:358
auto MakeSharedPtr(ARGS_TYPE &&... args) -> shared_ptr< T >
same as make_shared, but if type T has block allocation, then use block allocation for the 'shared pa...
conditional_t< qStroika_Foundation_Memory_PreferBlockAllocation and andTrueCheck, BlockAllocationUseHelper< T >, Common::Empty > UseBlockAllocationIfAppropriate
Use this to enable block allocation for a particular class. Beware of subclassing.
bool Equals(const T *lhs, const T *rhs)
strcmp or wsccmp() as appropriate == 0
#define CompileTimeFlagChecker_SOURCE(NS_PREFIX, NAME, VALUE)
String libXMLString2String(const xmlChar *s, int len)
Definition LibXML2.cpp:1104
#define Stroika_ATTRIBUTE_INDETERMINATE
Stroika_ATTRIBUTE_INDETERMINATE is used where you would use a C++ attribute for a variable that is in...
Definition StdCompat.h:428
#define Stroika_ATTRIBUTE_NO_UNIQUE_ADDRESS_VCFORCE
[[msvc::no_unique_address]] isn't always broken in MSVC. Annotate with this on things where its not b...
Definition StdCompat.h:481
#define DbgTrace
Definition Trace.h:327
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual String ReplaceAll(const RegularExpression &regEx, const String &with) const
Definition String.cpp:1214
nonvirtual size_t find(Character c, size_t startAt=0) const
Definition String.inl:1065
A Mapping uniquely associates two elements: a key and a value (use Assocation to allow multiple value...
nonvirtual bool Add(ArgByValueType< key_type > key, ArgByValueType< mapped_type > newElt, AddReplaceMode addReplaceMode=AddReplaceMode::eAddReplaces)
Definition Mapping.inl:186
A generalization of a vector: a container whose elements are keyed by the natural numbers.
virtual void StartElement(const Name &name, const Mapping< Name, String > &attributes)
Definition IConsumer.cpp:24
nonvirtual shared_ptr< IRep > GetRep() const
return the associated shared_ptr (cannot be nullptr)
Node::Ptr is a smart pointer to a Node::IRep.
Definition DOM.h:243
nonvirtual shared_ptr< IRep > GetRep() const
return the associated shared_ptr (cannot be nullptr)
Definition DOM.inl:99
NOT a real mutex - just a debugging infrastructure support tool so in debug builds can be assured thr...
shared_lock< const AssertExternallySynchronizedChecker > ReadContext
Instantiate AssertExternallySynchronizedChecker::ReadContext to designate an area of code where prote...
Exception<> is a replacement (subclass) for any std c++ exception class (e.g. the default 'std::excep...
Definition Exceptions.h:219
nonvirtual T As(optional< StringPCTEncodedFlag > pctEncoded={}) const
InputStream<>::Ptr is Smart pointer (with abstract Rep) class defining the interface to reading from ...
nonvirtual optional< ElementType > ReadBlocking() const
ReadBlocking () reads either a single element, or fills in argument intoBuffer - but never blocks (no...
nonvirtual SeekOffsetType Seek(SeekOffsetType offset) const
nonvirtual SeekOffsetType GetOffset() const
OutputStream<>::Ptr is Smart pointer to a stream-based sink of data.
nonvirtual void Write(span< ELEMENT_TYPE2, EXTENT_2 > elts) const
Iterable<T> is a base class for containers which easily produce an Iterator<T> to traverse them.
Definition Iterable.h:234
void ThrowIfNull(const Private_::ConstVoidStar &p, const HRESULT &hr)
Template specialization for ThrowIfNull (), for thing being thrown HRESULT - really throw HRESULTErro...
auto Finally(FUNCTION &&f) -> Private_::FinallySentry< FUNCTION >
Definition Finally.inl:31
An element's or attribute's name (local-name, not including a prefix), plus the namespace that name i...
Definition Namespace.h:63
String fName
The LOCAL name - "Why" for <x:Why>
Definition Namespace.h:67
optional< URI > fNamespace
The namespace fName is in, however the document said so - nullopt for a name in no namespace.
Definition Namespace.h:72