From fa23c3c527a148c8c6486c46587cbb8d32f8d800 Mon Sep 17 00:00:00 2001 From: Raul Metsma Date: Thu, 23 Jul 2026 12:49:27 +0300 Subject: [PATCH] Do not use libxml2 for XMLWriter IB-8732 Signed-off-by: Raul Metsma --- cdoc/DDocWriter.cpp | 2 +- cdoc/XmlWriter.cpp | 193 +++++++++++++++++++++++++++++--------------- cdoc/XmlWriter.h | 34 +++++--- 3 files changed, 150 insertions(+), 79 deletions(-) diff --git a/cdoc/DDocWriter.cpp b/cdoc/DDocWriter.cpp index 2133e8e4..0a0bfb41 100644 --- a/cdoc/DDocWriter.cpp +++ b/cdoc/DDocWriter.cpp @@ -27,7 +27,7 @@ using namespace libcdoc; * @brief DDOCWriter is used for storing multiple files. */ -constexpr XMLWriter::NS DDOC{ nullptr, "http://www.sk.ee/DigiDoc/v1.3.0#" }; +constexpr XMLWriter::NS DDOC{ {}, "http://www.sk.ee/DigiDoc/v1.3.0#" }; DDOCWriter::DDOCWriter(DataConsumer &dst) : XMLWriter(dst) diff --git a/cdoc/XmlWriter.cpp b/cdoc/XmlWriter.cpp index 7010559a..a6900135 100644 --- a/cdoc/XmlWriter.cpp +++ b/cdoc/XmlWriter.cpp @@ -20,68 +20,93 @@ #include "Io.h" -#include - +#include #include using namespace libcdoc; -using pcxmlChar = xmlChar *; +XMLWriter::XMLWriter(DataConsumer &dst) + : dst(dst) +{ + write(R"()"); +} -XMLWriter::XMLWriter(libcdoc::DataConsumer &dst) - : w(make_unique_ptr(xmlNewTextWriter(xmlOutputBufferCreateIO([](void *context, const char *buffer, int len) -> int { - auto *dst = reinterpret_cast(context); - auto result = dst->write((uint8_t *) buffer, len); - return result >= OK ? result : -1; - }, nullptr, &dst, nullptr)), xmlFreeTextWriter)) +XMLWriter::~XMLWriter() noexcept = default; + +// XML-escape `in` into the scratch buffer. In attribute context '"' is also escaped. +void XMLWriter::escape(std::string_view in, bool attribute) { - if(w) - xmlTextWriterStartDocument(w.get(), nullptr, "UTF-8", nullptr); + for(char c: in) + { + switch(c) + { + case '&': buf += "&"; break; + case '<': buf += "<"; break; + case '>': buf += ">"; break; + case '"': if(attribute) { buf += """; break; } [[fallthrough]]; + default: buf += c; + } + } } -XMLWriter::~XMLWriter() noexcept +int64_t XMLWriter::write(std::string_view str) { - if(w) - xmlTextWriterEndDocument(w.get()); + return dst.write(reinterpret_cast(str.data()), str.size()) < 0 ? IO_ERROR : OK; } -int64_t XMLWriter::writeStartElement(NS ns, const char *name, const std::map &attr) +int64_t XMLWriter::writeStartElement(NS ns, std::string_view name, const std::map &attr) { - if(!w) - return WRONG_ARGUMENTS; - auto &count = nsmap[ns.prefix ? ns.prefix : std::string_view{}]; - count++; - if(xmlTextWriterStartElementNS(w.get(), pcxmlChar(ns.prefix), pcxmlChar(name), count > 1 ? nullptr : pcxmlChar(ns.ns)) == -1) - return IO_ERROR; - for(const auto &[name, content]: attr) + std::string qname; + if(!ns.prefix.empty()) + (qname += ns.prefix) += ':'; + qname += name; + + buf.clear(); + buf += '<'; + buf += qname; + // Declare the namespace only on its first (outermost) open element; nested + // elements with the same prefix inherit it. + if(auto &count = nsmap[ns.prefix]; ++count == 1 && !ns.ns.empty()) { - if(xmlTextWriterWriteAttribute(w.get(), pcxmlChar(name), pcxmlChar(content.c_str())) == -1) - return IO_ERROR; + buf += !ns.prefix.empty() ? " xmlns:" : " xmlns"; + if(!ns.prefix.empty()) + buf += ns.prefix; + buf += "=\""; + buf += ns.ns; // namespace URIs are compile-time constants, no escaping needed + buf += '"'; } - return OK; + for(const auto &[aname, avalue]: attr) + { + (buf += ' ') += aname; + buf += "=\""; + escape(avalue, true); + buf += '"'; + } + buf += '>'; + stack.push_back(std::move(qname)); + return write(buf); } int64_t XMLWriter::writeEndElement(NS ns) { - if(!w) + if(stack.empty()) return WRONG_ARGUMENTS; - if(xmlTextWriterEndElement(w.get()) == -1) - return IO_ERROR; - if(auto pos = nsmap.find(ns.prefix ? ns.prefix : ""); pos != nsmap.cend()) + buf.clear(); + buf += "'; + stack.pop_back(); + if(auto pos = nsmap.find(ns.prefix); pos != nsmap.cend()) pos->second--; - return OK; + return write(buf); } -int64_t XMLWriter::writeElement(NS ns, const char *name, const std::function &f) +int64_t XMLWriter::writeElement(NS ns, std::string_view name, const std::function &f) { - if(auto rv = writeStartElement(ns, name, {}); rv != OK) - return rv; - if(int64_t rv = OK; f && (rv = f()) != OK) - return rv; - return writeEndElement(ns); + return writeElement(ns, name, {}, f); } -int64_t XMLWriter::writeElement(NS ns, const char *name, const std::map &attr, const std::function &f) +int64_t XMLWriter::writeElement(NS ns, std::string_view name, const std::map &attr, const std::function &f) { if(auto rv = writeStartElement(ns, name, attr); rv != OK) return rv; @@ -90,59 +115,94 @@ int64_t XMLWriter::writeElement(NS ns, const char *name, const std::map &f, const std::map &attr) +// Base64-encode `len` bytes straight into a stack buffer and bulk-write to dst, +// with no per-chunk heap allocation. `len` need not be a multiple of 3 — padding +// is emitted for the 1..2 byte remainder, so for streamed output pass whole +// triples on every chunk except the final tail (padding is then written once). +int64_t XMLWriter::writeBase64(const uint8_t *src, size_t len) +{ + constexpr char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::array buf; // multiple of 4; flushed once full + size_t n = 0, i = 0; + for(; i + 3 <= len; i += 3) + { + uint32_t v = uint32_t(src[i]) << 16 | uint32_t(src[i + 1]) << 8 | src[i + 2]; + buf[n++] = B64[v >> 18 & 63]; + buf[n++] = B64[v >> 12 & 63]; + buf[n++] = B64[v >> 6 & 63]; + buf[n++] = B64[v & 63]; + if(n == buf.size()) + { + if(dst.write(reinterpret_cast(buf.data()), n) < 0) + return IO_ERROR; + n = 0; + } + } + if(size_t rem = len - i; rem == 1) + { + uint32_t v = uint32_t(src[i]) << 16; + buf[n++] = B64[v >> 18 & 63]; + buf[n++] = B64[v >> 12 & 63]; + buf[n++] = '='; + buf[n++] = '='; + } + else if(rem == 2) + { + uint32_t v = uint32_t(src[i]) << 16 | uint32_t(src[i + 1]) << 8; + buf[n++] = B64[v >> 18 & 63]; + buf[n++] = B64[v >> 12 & 63]; + buf[n++] = B64[v >> 6 & 63]; + buf[n++] = '='; + } + if(n > 0 && dst.write(reinterpret_cast(buf.data()), n) < 0) + return IO_ERROR; + return OK; +} + +int64_t XMLWriter::writeBase64Element(NS ns, std::string_view name, const std::function &f, const std::map &attr) { if(auto rv = writeStartElement(ns, name, attr); rv != OK) return rv; - struct Base64Consumer : public DataConsumer { - xmlTextWriterPtr w; - std::array buf {}; // buffer up to 2 leftover bytes + struct Base64Consumer: public DataConsumer { + XMLWriter &w; + std::array buf {}; // up to 2 leftover bytes between writes size_t bufSize = 0; - Base64Consumer(xmlTextWriterPtr _w) : w(_w) {} + Base64Consumer(XMLWriter &w): w(w) {} result_t write(const uint8_t *src, size_t size) noexcept final { if(!src || size == 0) return OK; - size_t pos = 0; if(bufSize > 0) { pos = std::min(buf.size() - bufSize, size); std::copy(src, src + pos, buf.begin() + bufSize); bufSize += pos; - if (bufSize < 3) { + if(bufSize < 3) return result_t(size); - } - if (xmlTextWriterWriteBase64(w, reinterpret_cast(buf.data()), 0, buf.size()) == -1) + if(w.writeBase64(buf.data(), buf.size()) != OK) return IO_ERROR; bufSize = 0; } - - // Write largest contiguous chunk with length multiple of 3 + // Emit the largest chunk whose length is a multiple of 3 (no padding). size_t remaining = size - pos; - if(size_t fullTriples = remaining - (remaining % 3); fullTriples > 0) { - if (xmlTextWriterWriteBase64(w, reinterpret_cast(src), pos, fullTriples) == -1) + if(size_t fullTriples = remaining - remaining % 3; fullTriples > 0) { + if(w.writeBase64(src + pos, fullTriples) != OK) return IO_ERROR; pos += fullTriples; } - - // Buffer leftover (0..2) bytes for next write/close - if(bufSize = size - pos; bufSize > 0) { + // Buffer the trailing 0..2 bytes for the next write / close(). + if(bufSize = size - pos; bufSize > 0) std::copy(src + pos, src + size, buf.begin()); - } - return result_t(size); } result_t close() noexcept final { - if (bufSize > 0) { - // write remaining 1..2 bytes so base64 padding is applied only at the end - if(xmlTextWriterWriteBase64(w, reinterpret_cast(buf.data()), 0, bufSize) == -1) - return IO_ERROR; - } + if(bufSize > 0 && w.writeBase64(buf.data(), bufSize) != OK) + return IO_ERROR; bufSize = 0; return OK; } bool isError() noexcept final { return false; } - } base64Consumer {w.get()}; + } base64Consumer {*this}; if(auto rv = f(base64Consumer); rv < 0) return rv; if(auto rv = base64Consumer.close(); rv < 0) @@ -150,20 +210,23 @@ int64_t XMLWriter::writeBase64Element(NS ns, const char *name, const std::functi return writeEndElement(ns); } -int64_t XMLWriter::writeBase64Element(NS ns, const char *name, const std::vector &data, const std::map &attr) +int64_t XMLWriter::writeBase64Element(NS ns, std::string_view name, const std::vector &data, const std::map &attr) { if(auto rv = writeStartElement(ns, name, attr); rv != OK) return rv; - if(xmlTextWriterWriteBase64(w.get(), reinterpret_cast(data.data()), 0, data.size()) == -1) + if(!data.empty() && writeBase64(data.data(), data.size()) != OK) return IO_ERROR; return writeEndElement(ns); } -int64_t XMLWriter::writeTextElement(NS ns, const char *name, const std::map &attr, const std::string &data) +int64_t XMLWriter::writeTextElement(NS ns, std::string_view name, const std::map &attr, std::string_view data) { if(auto rv = writeStartElement(ns, name, attr); rv != OK) return rv; - if(xmlTextWriterWriteString(w.get(), pcxmlChar(data.c_str())) == -1) + // writeStartElement already flushed buf, so it is free to reuse for the text. + buf.clear(); + escape(data, false); + if(write(buf) != OK) return IO_ERROR; return writeEndElement(ns); } diff --git a/cdoc/XmlWriter.h b/cdoc/XmlWriter.h index 4f148f09..6654a723 100644 --- a/cdoc/XmlWriter.h +++ b/cdoc/XmlWriter.h @@ -18,14 +18,12 @@ #pragma once -#include "utils/memory.h" - #include #include #include #include - -struct _xmlTextWriter; +#include +#include namespace libcdoc { @@ -34,21 +32,31 @@ struct DataConsumer; class XMLWriter { public: - struct NS { const char *prefix, *ns; }; + struct NS { std::string_view prefix, ns; }; - XMLWriter(DataConsumer &dst); virtual ~XMLWriter() noexcept; - int64_t writeStartElement(NS ns, const char *name, const std::map &attr); +protected: + // XMLWriter is a base class for concrete writers (DDOCWriter, CDoc1Writer); + // the element-building vocabulary is only used by subclasses. + XMLWriter(DataConsumer &dst); + + int64_t writeStartElement(NS ns, std::string_view name, const std::map &attr); int64_t writeEndElement(NS ns); - int64_t writeElement(NS ns, const char *name, const std::function &f = nullptr); - int64_t writeElement(NS ns, const char *name, const std::map &attr, const std::function &f = nullptr); - int64_t writeBase64Element(NS ns, const char *name, const std::function &f, const std::map &attr = {}); - int64_t writeBase64Element(NS ns, const char *name, const std::vector &data, const std::map &attr = {}); - int64_t writeTextElement(NS ns, const char *name, const std::map &attr, const std::string &data); + int64_t writeElement(NS ns, std::string_view name, const std::function &f = nullptr); + int64_t writeElement(NS ns, std::string_view name, const std::map &attr, const std::function &f = nullptr); + int64_t writeBase64Element(NS ns, std::string_view name, const std::function &f, const std::map &attr = {}); + int64_t writeBase64Element(NS ns, std::string_view name, const std::vector &data, const std::map &attr = {}); + int64_t writeTextElement(NS ns, std::string_view name, const std::map &attr, std::string_view data); private: - unique_free_t<_xmlTextWriter> w; + int64_t write(std::string_view str); + int64_t writeBase64(const uint8_t *src, size_t len); // encodes directly to dst + void escape(std::string_view in, bool attribute); // XML-escapes into buf + + DataConsumer &dst; + std::string buf; // reusable scratch for building tags/text + std::vector stack; // open element qualified names, for end tags std::map nsmap; };