Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cdoc/DDocWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
193 changes: 128 additions & 65 deletions cdoc/XmlWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,68 +20,93 @@

#include "Io.h"

#include <libxml/xmlwriter.h>

#include <algorithm>
#include <array>

using namespace libcdoc;

using pcxmlChar = xmlChar *;
XMLWriter::XMLWriter(DataConsumer &dst)
: dst(dst)
{
write(R"(<?xml version="1.0" encoding="UTF-8"?>)");
}

XMLWriter::XMLWriter(libcdoc::DataConsumer &dst)
: w(make_unique_ptr(xmlNewTextWriter(xmlOutputBufferCreateIO([](void *context, const char *buffer, int len) -> int {
auto *dst = reinterpret_cast<libcdoc::DataConsumer *>(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 += "&amp;"; break;
case '<': buf += "&lt;"; break;
case '>': buf += "&gt;"; break;
case '"': if(attribute) { buf += "&quot;"; 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<const uint8_t*>(str.data()), str.size()) < 0 ? IO_ERROR : OK;
}

int64_t XMLWriter::writeStartElement(NS ns, const char *name, const std::map<const char *, std::string> &attr)
int64_t XMLWriter::writeStartElement(NS ns, std::string_view name, const std::map<std::string_view, std::string> &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 += "</";
buf += stack.back();
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<int64_t()> &f)
int64_t XMLWriter::writeElement(NS ns, std::string_view name, const std::function<int64_t()> &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<const char *, std::string> &attr, const std::function<int64_t()> &f)
int64_t XMLWriter::writeElement(NS ns, std::string_view name, const std::map<std::string_view, std::string> &attr, const std::function<int64_t()> &f)
{
if(auto rv = writeStartElement(ns, name, attr); rv != OK)
return rv;
Expand All @@ -90,80 +115,118 @@ int64_t XMLWriter::writeElement(NS ns, const char *name, const std::map<const ch
return writeEndElement(ns);
}

int64_t XMLWriter::writeBase64Element(NS ns, const char *name, const std::function<int64_t(DataConsumer&)> &f, const std::map<const char *, std::string> &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<char, 4092> 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<const uint8_t*>(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<const uint8_t*>(buf.data()), n) < 0)
return IO_ERROR;
return OK;
}

int64_t XMLWriter::writeBase64Element(NS ns, std::string_view name, const std::function<int64_t(DataConsumer&)> &f, const std::map<std::string_view, std::string> &attr)
{
if(auto rv = writeStartElement(ns, name, attr); rv != OK)
return rv;

struct Base64Consumer : public DataConsumer {
xmlTextWriterPtr w;
std::array<uint8_t, 3> buf {}; // buffer up to 2 leftover bytes
struct Base64Consumer: public DataConsumer {
XMLWriter &w;
std::array<uint8_t, 3> 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<const char*>(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<const char*>(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<const char*>(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)
return rv;
return writeEndElement(ns);
}

int64_t XMLWriter::writeBase64Element(NS ns, const char *name, const std::vector<xmlChar> &data, const std::map<const char *, std::string> &attr)
int64_t XMLWriter::writeBase64Element(NS ns, std::string_view name, const std::vector<unsigned char> &data, const std::map<std::string_view, std::string> &attr)
{
if(auto rv = writeStartElement(ns, name, attr); rv != OK)
return rv;
if(xmlTextWriterWriteBase64(w.get(), reinterpret_cast<const char*>(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<const char *, std::string> &attr, const std::string &data)
int64_t XMLWriter::writeTextElement(NS ns, std::string_view name, const std::map<std::string_view, std::string> &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);
}
34 changes: 21 additions & 13 deletions cdoc/XmlWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@

#pragma once

#include "utils/memory.h"

#include <cstdint>
#include <functional>
#include <map>
#include <string>

struct _xmlTextWriter;
#include <string_view>
#include <vector>

namespace libcdoc {

Expand All @@ -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<const char *, std::string> &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<std::string_view, std::string> &attr);
int64_t writeEndElement(NS ns);
int64_t writeElement(NS ns, const char *name, const std::function<int64_t()> &f = nullptr);
int64_t writeElement(NS ns, const char *name, const std::map<const char *, std::string> &attr, const std::function<int64_t()> &f = nullptr);
int64_t writeBase64Element(NS ns, const char *name, const std::function<int64_t(DataConsumer &)> &f, const std::map<const char *, std::string> &attr = {});
int64_t writeBase64Element(NS ns, const char *name, const std::vector<unsigned char> &data, const std::map<const char *, std::string> &attr = {});
int64_t writeTextElement(NS ns, const char *name, const std::map<const char *, std::string> &attr, const std::string &data);
int64_t writeElement(NS ns, std::string_view name, const std::function<int64_t()> &f = nullptr);
int64_t writeElement(NS ns, std::string_view name, const std::map<std::string_view, std::string> &attr, const std::function<int64_t()> &f = nullptr);
int64_t writeBase64Element(NS ns, std::string_view name, const std::function<int64_t(DataConsumer &)> &f, const std::map<std::string_view, std::string> &attr = {});
int64_t writeBase64Element(NS ns, std::string_view name, const std::vector<unsigned char> &data, const std::map<std::string_view, std::string> &attr = {});
int64_t writeTextElement(NS ns, std::string_view name, const std::map<std::string_view, std::string> &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<std::string> stack; // open element qualified names, for end tags
std::map<std::string_view, int> nsmap;
};

Expand Down