From ce435683116e672faefbdafed44aef33c03da880 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 | 263 ++++++++++++++++++++++++++++++++------------ cdoc/XmlWriter.h | 33 +++--- 3 files changed, 214 insertions(+), 84 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..6605fcfd 100644 --- a/cdoc/XmlWriter.cpp +++ b/cdoc/XmlWriter.cpp @@ -19,69 +19,167 @@ #include "XmlWriter.h" #include "Io.h" +#include "Utils.h" -#include - +#include #include +namespace { + +static constexpr void encodeBase64Group(const uint8_t *src, size_t count, char *out) +{ + constexpr char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + uint32_t v = uint32_t(src[0]) << 16; + if(count > 1) + v |= uint32_t(src[1]) << 8; + if(count > 2) + v |= src[2]; + out[0] = B64[v >> 18 & 63]; + out[1] = B64[v >> 12 & 63]; + out[2] = count > 1 ? B64[v >> 6 & 63] : '='; + out[3] = count > 2 ? B64[v & 63] : '='; +} + +constexpr bool isValidXmlText(std::string_view str) +{ + for (size_t pos = 0; pos < str.size();) { + auto byte = [&](size_t i) { return static_cast(str[pos + i]); }; + char32_t c; + size_t len; + if (byte(0) < 0x80) { c = byte(0); len = 1; } + else if ((byte(0) & 0xe0) == 0xc0) { c = byte(0) & 0x1f; len = 2; } + else if ((byte(0) & 0xf0) == 0xe0) { c = byte(0) & 0x0f; len = 3; } + else if ((byte(0) & 0xf8) == 0xf0) { c = byte(0) & 0x07; len = 4; } + else return false; + if (str.size() - pos < len) return false; + for (size_t i = 1; i < len; ++i) { + if ((byte(i) & 0xc0) != 0x80) return false; + c = c << 6 | (byte(i) & 0x3f); + } + constexpr char32_t minValue[] {0, 0, 0x80, 0x800, 0x10000}; + if (c < minValue[len]) return false; // overlong encoding + // XML 1.0 Char: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + if (!(c == 0x9 || c == 0xA || c == 0xD || + (c >= 0x20 && c <= 0xD7FF) || + (c >= 0xE000 && c <= 0xFFFD) || + (c >= 0x10000 && c <= 0x10FFFF))) + return false; + pos += len; + } + return true; +} + +static_assert([] { + constexpr uint8_t in[] {'M', 'a', 'n'}; + char out[12] {}; + encodeBase64Group(in, 3, out); + encodeBase64Group(in, 2, out + 4); + encodeBase64Group(in, 1, out + 8); + return std::string_view(out, 12) == "TWFuTWE=TQ=="; +}()); + +static_assert(isValidXmlText("a\tb\nc\rd \xC3\xB5 \xF0\x9F\x98\x80")); +static_assert(!isValidXmlText("a\x01" "b")); +static_assert(!isValidXmlText("\xED\xA0\x80")); + +} + using namespace libcdoc; -using pcxmlChar = xmlChar *; +XMLWriter::XMLWriter(DataConsumer &dst) + : dst(dst) +{} -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)) +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 '\r': buf += " "; break; + case '\t': buf += attribute ? " " : "\t"; break; + case '\n': buf += attribute ? " " : "\n"; break; + case '"': buf += attribute ? """ : "\""; break; + default: buf += c; + } + } } -XMLWriter::~XMLWriter() noexcept +int64_t XMLWriter::write(std::string_view str) noexcept { - if(w) - xmlTextWriterEndDocument(w.get()); + if(str.empty()) + return OK; + if(auto rv = dst.write(reinterpret_cast(str.data()), str.size()); rv < 0) + return rv; + return 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) + // Validate before touching nsmap/stack so a rejected element leaves no state behind. + for(const auto &[aname, avalue]: attr) + if(!isValidXmlText(avalue)) + return DATA_FORMAT_ERROR; + std::string qname; + if(!ns.prefix.empty()) + (qname += ns.prefix) += ':'; + qname += name; + + if(!isStarted) { - if(xmlTextWriterWriteAttribute(w.get(), pcxmlChar(name), pcxmlChar(content.c_str())) == -1) - return IO_ERROR; + if(auto rv = write(""); rv != OK) + return rv; + isStarted = true; } - return OK; + 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()) + { + 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 += '"'; + } + 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 +188,79 @@ int64_t XMLWriter::writeElement(NS ns, const char *name, const std::map &f, const std::map &attr) +int64_t XMLWriter::writeBase64(const uint8_t *src, size_t len) noexcept +{ + if(!src || len == 0) + return OK; + std::array buf; // multiple of 4; flushed once full + size_t n = 0, i = 0; + for(; i + 3 <= len; i += 3) + { + encodeBase64Group(src + i, 3, buf.data() + n); + if(n += 4; n == buf.size()) + { + if(auto rv = write({buf.data(), n}); rv < 0) + return rv; + n = 0; + } + } + if(size_t rem = len - i; rem > 0) + { + encodeBase64Group(src + i, rem, buf.data() + n); + n += 4; + } + return write({buf.data(), n}); +} + +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) {} + result_t error = OK; + Base64Consumer(XMLWriter &w): w(w) {} result_t write(const uint8_t *src, size_t size) noexcept final { + if(error != OK) + return error; 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) - return IO_ERROR; + if(auto rv = w.writeBase64(buf.data(), buf.size()); rv != OK) + return error = rv; 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) - return IO_ERROR; + if(size_t fullTriples = remaining - remaining % 3; fullTriples > 0) { + if(auto rv = w.writeBase64(src + pos, fullTriples); rv != OK) + return error = rv; 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(error != OK) + return error; + if(auto rv = w.writeBase64(buf.data(), bufSize); rv != OK) + return error = rv; bufSize = 0; return OK; } - bool isError() noexcept final { return false; } - } base64Consumer {w.get()}; + bool isError() noexcept final { return error != OK; } + } base64Consumer {*this}; if(auto rv = f(base64Consumer); rv < 0) return rv; if(auto rv = base64Consumer.close(); rv < 0) @@ -150,20 +268,25 @@ 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) - return IO_ERROR; + if(auto rv = writeBase64(data.data(), data.size()); rv != OK) + return rv; 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(!isValidXmlText(data)) + return DATA_FORMAT_ERROR; if(auto rv = writeStartElement(ns, name, attr); rv != OK) return rv; - if(xmlTextWriterWriteString(w.get(), pcxmlChar(data.c_str())) == -1) - return IO_ERROR; + // writeStartElement already flushed buf, so it is free to reuse for the text. + buf.clear(); + escape(data, false); + if(auto rv = write(buf); rv != OK) + return rv; return writeEndElement(ns); } diff --git a/cdoc/XmlWriter.h b/cdoc/XmlWriter.h index 4f148f09..1e6e137e 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,30 @@ struct DataConsumer; class XMLWriter { public: - struct NS { const char *prefix, *ns; }; + struct NS { std::string_view prefix, ns; }; + + virtual ~XMLWriter() noexcept = default; +protected: XMLWriter(DataConsumer &dst); - virtual ~XMLWriter() noexcept; - int64_t writeStartElement(NS ns, const char *name, const std::map &attr); + 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) noexcept; + int64_t writeBase64(const uint8_t *src, size_t len) noexcept; + void escape(std::string_view in, bool attribute); + + DataConsumer &dst; + bool isStarted = false; + std::string buf; + std::vector stack; std::map nsmap; };