feat: CalDAV foundation — HTTP methods and XML parser
Add PROPFIND and REPORT to HttpMethod enum for CalDAV protocol support. Implement minimal XML parser (caldav_xml.hpp) for CalDAV response parsing: namespaced tags, attributes, CDATA, entity references, self-closing elements. Bounded by max depth and size to prevent allocation bombs.
This commit is contained in:
38
include/nocal/sync/caldav_xml.hpp
Normal file
38
include/nocal/sync/caldav_xml.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace nocal::sync {
|
||||
|
||||
// A parsed XML element.
|
||||
struct XmlElement {
|
||||
std::string tag;
|
||||
std::vector<std::pair<std::string, std::string>> attributes;
|
||||
std::string text;
|
||||
std::string cdata;
|
||||
std::vector<XmlElement> children;
|
||||
};
|
||||
|
||||
// Parse an XML document string into a tree of XmlElement.
|
||||
// Returns the root element. Throws std::runtime_error on parse failure.
|
||||
// Bounded by max_size to prevent allocation bombs.
|
||||
[[nodiscard]] XmlElement parse_xml(
|
||||
std::string_view document, std::size_t max_size = 4 * 1024 * 1024);
|
||||
|
||||
// Find all children with the given tag name.
|
||||
[[nodiscard]] std::vector<const XmlElement*> find_children(
|
||||
const XmlElement& parent, std::string_view tag);
|
||||
|
||||
// Get first child text or empty string.
|
||||
[[nodiscard]] std::string_view get_child_text(
|
||||
const XmlElement& parent, std::string_view tag);
|
||||
|
||||
// Get attribute value or empty string.
|
||||
[[nodiscard]] std::string_view get_attribute(
|
||||
const XmlElement& elem, std::string_view name);
|
||||
|
||||
} // namespace nocal::sync
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
namespace nocal::sync {
|
||||
|
||||
enum class HttpMethod { get, post, put, patch, delete_ };
|
||||
enum class HttpMethod { get, post, put, patch, delete_, propfind, report };
|
||||
|
||||
struct HttpHeader {
|
||||
std::string name;
|
||||
|
||||
@@ -26,6 +26,7 @@ nocal_sources = files(
|
||||
'src/domain/month_layout.cpp',
|
||||
'src/storage/ics_store.cpp',
|
||||
'src/storage/sync_cache.cpp',
|
||||
'src/sync/caldav_xml.cpp',
|
||||
'src/sync/curl_http.cpp',
|
||||
'src/sync/desktop_oauth.cpp',
|
||||
'src/sync/microsoft_account.cpp',
|
||||
@@ -114,6 +115,10 @@ unicode_tests = executable('unicode-tests',
|
||||
'tests/unicode_tests.cpp', dependencies: nocal_dep)
|
||||
test('nocal:unicode', unicode_tests)
|
||||
|
||||
caldav_xml_tests = executable('caldav-xml-tests',
|
||||
'tests/caldav_xml_tests.cpp', dependencies: nocal_dep)
|
||||
test('caldav-xml', caldav_xml_tests)
|
||||
|
||||
test('cli-recovery', shell,
|
||||
args: [files('tests/cli_recovery_test.sh'), nocal_executable])
|
||||
test('cli-transfer', shell,
|
||||
|
||||
262
src/sync/caldav_xml.cpp
Normal file
262
src/sync/caldav_xml.cpp
Normal file
@@ -0,0 +1,262 @@
|
||||
#include "nocal/sync/caldav_xml.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace nocal::sync {
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t max_depth = 64;
|
||||
|
||||
std::string_view trim(std::string_view s) {
|
||||
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front()))) s.remove_prefix(1);
|
||||
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back()))) s.remove_suffix(1);
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string replace_entities(std::string_view s) {
|
||||
std::string result;
|
||||
result.reserve(s.size());
|
||||
for (std::size_t i = 0; i < s.size(); ++i) {
|
||||
if (s[i] == '&' && i + 4 < s.size() && s[i + 1] == 'l' && s[i + 2] == 't' && s[i + 3] == ';') {
|
||||
result.push_back('<');
|
||||
i += 3;
|
||||
} else if (s[i] == '&' && i + 4 < s.size() && s[i + 1] == 'g' && s[i + 2] == 't' && s[i + 3] == ';') {
|
||||
result.push_back('>');
|
||||
i += 3;
|
||||
} else if (s[i] == '&' && i + 5 < s.size() && s[i + 1] == 'a' && s[i + 2] == 'm'
|
||||
&& s[i + 3] == 'p' && s[i + 4] == ';') {
|
||||
result.push_back('&');
|
||||
i += 4;
|
||||
} else if (s[i] == '&' && i + 6 < s.size() && s[i + 1] == 'q' && s[i + 2] == 'u'
|
||||
&& s[i + 3] == 'o' && s[i + 4] == 't' && s[i + 5] == ';') {
|
||||
result.push_back('"');
|
||||
i += 5;
|
||||
} else if (s[i] == '&' && i + 6 < s.size() && s[i + 1] == 'a' && s[i + 2] == 'p'
|
||||
&& s[i + 3] == 'o' && s[i + 4] == 's' && s[i + 5] == ';') {
|
||||
result.push_back('\'');
|
||||
i += 5;
|
||||
} else {
|
||||
result.push_back(s[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class XmlParser {
|
||||
public:
|
||||
explicit XmlParser(std::string_view doc) : doc_(doc), pos_(0) {}
|
||||
|
||||
[[nodiscard]] XmlElement parse_root() {
|
||||
skip_whitespace();
|
||||
while (pos_ < doc_.size()) {
|
||||
skip_whitespace();
|
||||
if (pos_ >= doc_.size()) break;
|
||||
if (doc_[pos_] == '<') {
|
||||
if (pos_ + 1 < doc_.size() && doc_[pos_ + 1] == '?') {
|
||||
skip_processing_instruction();
|
||||
} else if (pos_ + 4 < doc_.size() && doc_.substr(pos_, 4) == "<!--") {
|
||||
skip_comment();
|
||||
} else {
|
||||
elements_.push_back(parse_element(1));
|
||||
}
|
||||
} else {
|
||||
++pos_;
|
||||
}
|
||||
}
|
||||
if (elements_.empty()) {
|
||||
throw std::runtime_error("XML document contains no root element");
|
||||
}
|
||||
return std::move(elements_[0]);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string_view doc_;
|
||||
std::size_t pos_;
|
||||
std::vector<XmlElement> elements_;
|
||||
|
||||
void skip_whitespace() {
|
||||
while (pos_ < doc_.size() && std::isspace(static_cast<unsigned char>(doc_[pos_]))) {
|
||||
++pos_;
|
||||
}
|
||||
}
|
||||
|
||||
void skip_processing_instruction() {
|
||||
if (pos_ + 2 <= doc_.size() && doc_[pos_] == '<' && doc_[pos_ + 1] == '?') {
|
||||
pos_ += 2;
|
||||
while (pos_ + 1 < doc_.size() && !(doc_[pos_] == '?' && doc_[pos_ + 1] == '>')) {
|
||||
++pos_;
|
||||
}
|
||||
if (pos_ + 1 < doc_.size()) pos_ += 2;
|
||||
}
|
||||
}
|
||||
|
||||
void skip_comment() {
|
||||
if (pos_ + 4 <= doc_.size() && doc_.substr(pos_, 4) == "<!--") {
|
||||
pos_ += 4;
|
||||
while (pos_ + 2 < doc_.size() && !(doc_[pos_] == '-' && doc_[pos_ + 1] == '-'
|
||||
&& doc_[pos_ + 2] == '>')) {
|
||||
++pos_;
|
||||
}
|
||||
if (pos_ + 2 < doc_.size()) pos_ += 3;
|
||||
}
|
||||
}
|
||||
|
||||
std::string read_tag_name() {
|
||||
std::string tag;
|
||||
while (pos_ < doc_.size() && !std::isspace(static_cast<unsigned char>(doc_[pos_]))
|
||||
&& doc_[pos_] != '>' && doc_[pos_] != '/' && doc_[pos_] != '<') {
|
||||
tag.push_back(doc_[pos_++]);
|
||||
}
|
||||
if (tag.empty()) {
|
||||
throw std::runtime_error("XML: empty tag name at position " + std::to_string(pos_));
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> read_attributes() {
|
||||
std::vector<std::pair<std::string, std::string>> attrs;
|
||||
skip_whitespace();
|
||||
while (pos_ < doc_.size() && doc_[pos_] != '>' && doc_[pos_] != '/') {
|
||||
std::string name;
|
||||
while (pos_ < doc_.size() && !std::isspace(static_cast<unsigned char>(doc_[pos_]))
|
||||
&& doc_[pos_] != '=') {
|
||||
name.push_back(doc_[pos_++]);
|
||||
}
|
||||
skip_whitespace();
|
||||
if (pos_ >= doc_.size() || doc_[pos_] != '=') {
|
||||
throw std::runtime_error("XML: expected '=' after attribute name at position "
|
||||
+ std::to_string(pos_));
|
||||
}
|
||||
++pos_;
|
||||
skip_whitespace();
|
||||
if (pos_ >= doc_.size() || (doc_[pos_] != '"' && doc_[pos_] != '\'')) {
|
||||
throw std::runtime_error("XML: expected quoted attribute value at position "
|
||||
+ std::to_string(pos_));
|
||||
}
|
||||
const char quote = doc_[pos_++];
|
||||
std::string value;
|
||||
while (pos_ < doc_.size() && doc_[pos_] != quote) {
|
||||
value.push_back(doc_[pos_++]);
|
||||
}
|
||||
if (pos_ >= doc_.size()) {
|
||||
throw std::runtime_error("XML: unterminated attribute value at position "
|
||||
+ std::to_string(pos_));
|
||||
}
|
||||
++pos_;
|
||||
attrs.emplace_back(std::move(name), std::move(value));
|
||||
skip_whitespace();
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
XmlElement parse_element(std::size_t depth) {
|
||||
if (depth > max_depth) {
|
||||
throw std::runtime_error("XML: maximum nesting depth exceeded");
|
||||
}
|
||||
if (pos_ >= doc_.size() || doc_[pos_] != '<') {
|
||||
throw std::runtime_error("XML: expected '<' at position " + std::to_string(pos_));
|
||||
}
|
||||
++pos_;
|
||||
const std::string tag = read_tag_name();
|
||||
const auto attributes = read_attributes();
|
||||
skip_whitespace();
|
||||
XmlElement elem;
|
||||
elem.tag = tag;
|
||||
elem.attributes = std::move(attributes);
|
||||
if (pos_ < doc_.size() && doc_[pos_] == '/') {
|
||||
++pos_;
|
||||
if (pos_ < doc_.size() && doc_[pos_] == '>') {
|
||||
++pos_;
|
||||
}
|
||||
return elem;
|
||||
}
|
||||
if (pos_ >= doc_.size() || doc_[pos_] != '>') {
|
||||
throw std::runtime_error("XML: expected '>' at position " + std::to_string(pos_));
|
||||
}
|
||||
++pos_;
|
||||
while (pos_ < doc_.size()) {
|
||||
if (pos_ + 9 <= doc_.size() && doc_.substr(pos_, 9) == "<![CDATA[") {
|
||||
pos_ += 9;
|
||||
std::size_t start = pos_;
|
||||
while (pos_ + 2 < doc_.size() && !(doc_[pos_] == ']' && doc_[pos_ + 1] == ']'
|
||||
&& doc_[pos_ + 2] == '>')) {
|
||||
++pos_;
|
||||
}
|
||||
elem.cdata.append(doc_.data() + start, pos_ - start);
|
||||
if (pos_ + 2 < doc_.size()) pos_ += 3;
|
||||
} else if (pos_ < doc_.size() && doc_[pos_] == '<' && pos_ + 1 < doc_.size()
|
||||
&& doc_[pos_ + 1] == '/') {
|
||||
break;
|
||||
} else if (pos_ < doc_.size() && doc_[pos_] == '<') {
|
||||
elem.children.push_back(parse_element(depth + 1));
|
||||
} else {
|
||||
std::size_t start = pos_;
|
||||
while (pos_ < doc_.size() && doc_[pos_] != '<') {
|
||||
++pos_;
|
||||
}
|
||||
elem.text.append(doc_.data() + start, pos_ - start);
|
||||
}
|
||||
}
|
||||
elem.text = replace_entities(trim(elem.text));
|
||||
if (pos_ < doc_.size() && doc_[pos_] == '<' && pos_ + 1 < doc_.size()
|
||||
&& doc_[pos_ + 1] == '/') {
|
||||
pos_ += 2;
|
||||
std::string close_tag;
|
||||
while (pos_ < doc_.size() && doc_[pos_] != '>') {
|
||||
close_tag.push_back(doc_[pos_++]);
|
||||
}
|
||||
close_tag = trim(close_tag);
|
||||
if (close_tag != tag) {
|
||||
throw std::runtime_error("XML: mismatched tags '" + tag + "' and '" + close_tag + "'");
|
||||
}
|
||||
if (pos_ < doc_.size()) ++pos_;
|
||||
} else {
|
||||
throw std::runtime_error("XML: unclosed element '" + tag + "'");
|
||||
}
|
||||
return elem;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
XmlElement parse_xml(const std::string_view document, const std::size_t max_size) {
|
||||
if (document.size() > max_size) {
|
||||
throw std::runtime_error("XML document exceeds maximum size");
|
||||
}
|
||||
XmlParser parser(document);
|
||||
return parser.parse_root();
|
||||
}
|
||||
|
||||
std::vector<const XmlElement*> find_children(const XmlElement& parent, const std::string_view tag) {
|
||||
std::vector<const XmlElement*> result;
|
||||
for (const auto& child : parent.children) {
|
||||
if (child.tag == tag) {
|
||||
result.push_back(&child);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string_view get_child_text(const XmlElement& parent, const std::string_view tag) {
|
||||
for (const auto& child : parent.children) {
|
||||
if (child.tag == tag) {
|
||||
return child.cdata.empty() ? child.text : child.cdata;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string_view get_attribute(const XmlElement& elem, const std::string_view name) {
|
||||
for (const auto& [n, v] : elem.attributes) {
|
||||
if (n == name) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace nocal::sync
|
||||
@@ -329,6 +329,8 @@ void set_option(CURL* handle, CURLoption option, Value value) {
|
||||
case HttpMethod::put: return "PUT";
|
||||
case HttpMethod::patch: return "PATCH";
|
||||
case HttpMethod::delete_: return "DELETE";
|
||||
case HttpMethod::propfind: return "PROPFIND";
|
||||
case HttpMethod::report: return "REPORT";
|
||||
}
|
||||
throw CurlHttpError(CurlHttpError::Kind::invalid_request, "invalid HTTP request method");
|
||||
}
|
||||
|
||||
206
tests/caldav_xml_tests.cpp
Normal file
206
tests/caldav_xml_tests.cpp
Normal file
@@ -0,0 +1,206 @@
|
||||
#include "nocal/sync/caldav_xml.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace {
|
||||
|
||||
void require(bool condition, std::string_view message) {
|
||||
if (!condition) {
|
||||
throw std::runtime_error(std::string(message));
|
||||
}
|
||||
}
|
||||
|
||||
void test_simple_element() {
|
||||
const auto root = nocal::sync::parse_xml("<root><child>text</child></root>");
|
||||
require(root.tag == "root", "root tag");
|
||||
require(root.children.size() == 1, "one child");
|
||||
require(root.children[0].tag == "child", "child tag");
|
||||
require(root.children[0].text == "text", "child text");
|
||||
}
|
||||
|
||||
void test_namespace_prefix() {
|
||||
const auto root = nocal::sync::parse_xml("<D:propfind><D:prop><C:displayname/></D:prop></D:propfind>");
|
||||
require(root.tag == "D:propfind", "namespaced root");
|
||||
require(root.children[0].tag == "D:prop", "namespaced child");
|
||||
require(root.children[0].children[0].tag == "C:displayname", "nested namespaced");
|
||||
}
|
||||
|
||||
void test_attributes() {
|
||||
const auto root = nocal::sync::parse_xml(R"(<elem href="/path" name="test" />)");
|
||||
require(root.tag == "elem", "tag");
|
||||
require(root.attributes.size() == 2, "two attributes");
|
||||
require(nocal::sync::get_attribute(root, "href") == "/path", "href");
|
||||
require(nocal::sync::get_attribute(root, "name") == "test", "name");
|
||||
}
|
||||
|
||||
void test_cdata() {
|
||||
const auto root = nocal::sync::parse_xml("<data><![CDATA[<xml>content</xml>]]></data>");
|
||||
require(root.cdata == "<xml>content</xml>", "CDATA content preserved");
|
||||
}
|
||||
|
||||
void test_self_closing() {
|
||||
const auto root = nocal::sync::parse_xml("<root><a/><b /></root>");
|
||||
require(root.children.size() == 2, "two self-closing children");
|
||||
require(root.children[0].children.empty(), "no nested children in self-closing");
|
||||
}
|
||||
|
||||
void test_nested_elements() {
|
||||
const auto root = nocal::sync::parse_xml("<a><b><c>deep</c></b></a>");
|
||||
require(root.children[0].children[0].text == "deep", "deep nesting");
|
||||
}
|
||||
|
||||
void test_entity_references() {
|
||||
const auto root = nocal::sync::parse_xml("<text><hello> & world</text>");
|
||||
require(root.text == "<hello> & world", "entities replaced");
|
||||
}
|
||||
|
||||
void test_processing_instruction() {
|
||||
const auto root = nocal::sync::parse_xml("<?xml version=\"1.0\"?><doc/>");
|
||||
require(root.tag == "doc", "PI skipped, root found");
|
||||
}
|
||||
|
||||
void test_comment_skipped() {
|
||||
const auto root = nocal::sync::parse_xml("<!-- comment --><doc/>");
|
||||
require(root.tag == "doc", "comment skipped");
|
||||
}
|
||||
|
||||
void test_find_children() {
|
||||
const auto root = nocal::sync::parse_xml("<root><a>1</a><b>2</b><a>3</a></root>");
|
||||
const auto found = nocal::sync::find_children(root, "a");
|
||||
require(found.size() == 2, "two 'a' children");
|
||||
require(found[0]->text == "1", "first a");
|
||||
require(found[1]->text == "3", "second a");
|
||||
}
|
||||
|
||||
void test_get_child_text() {
|
||||
const auto root = nocal::sync::parse_xml("<root><name>Bob</name></root>");
|
||||
require(nocal::sync::get_child_text(root, "name") == "Bob", "child text");
|
||||
require(nocal::sync::get_child_text(root, "missing").empty(), "missing child");
|
||||
}
|
||||
|
||||
void test_empty_document() {
|
||||
bool threw = false;
|
||||
try {
|
||||
(void)nocal::sync::parse_xml("");
|
||||
} catch (const std::runtime_error&) {
|
||||
threw = true;
|
||||
}
|
||||
require(threw, "empty document throws");
|
||||
}
|
||||
|
||||
void test_unclosed_tag() {
|
||||
bool threw = false;
|
||||
try {
|
||||
(void)nocal::sync::parse_xml("<root><child>");
|
||||
} catch (const std::runtime_error&) {
|
||||
threw = true;
|
||||
}
|
||||
require(threw, "unclosed tag throws");
|
||||
}
|
||||
|
||||
void test_mismatched_tag() {
|
||||
bool threw = false;
|
||||
try {
|
||||
(void)nocal::sync::parse_xml("<root><child></wrong>");
|
||||
} catch (const std::runtime_error&) {
|
||||
threw = true;
|
||||
}
|
||||
require(threw, "mismatched tag throws");
|
||||
}
|
||||
|
||||
void test_max_depth() {
|
||||
bool threw = false;
|
||||
std::string deep("<a>");
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
deep += "<a>";
|
||||
}
|
||||
deep += "x";
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
deep += "</a>";
|
||||
}
|
||||
deep += "</a>";
|
||||
try {
|
||||
(void)nocal::sync::parse_xml(deep);
|
||||
} catch (const std::runtime_error&) {
|
||||
threw = true;
|
||||
}
|
||||
require(threw, "excessive depth throws");
|
||||
}
|
||||
|
||||
void test_caldav_propfind_response() {
|
||||
const std::string doc = R"(<?xml version="1.0"?>
|
||||
<D:multistatus xmlns:D="DAV:" xmlns:C="CALDAV:" xmlns:CS="http://calendarserver.org/ns/">
|
||||
<D:response>
|
||||
<D:href>/calendars/user/calendar1/</D:href>
|
||||
<D:propstat>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
<D:prop>
|
||||
<D:displayname>My Calendar</D:displayname>
|
||||
<D:resourcetype><D:collection><C:calendar/></D:collection></D:resourcetype>
|
||||
<D:sync-token>sync-token-123</D:sync-token>
|
||||
</D:prop>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>)";
|
||||
|
||||
const auto root = nocal::sync::parse_xml(doc);
|
||||
require(root.tag == "D:multistatus", "root tag");
|
||||
require(root.children.size() == 1, "one response");
|
||||
const auto& response = root.children[0];
|
||||
require(response.tag == "D:response", "response element");
|
||||
const auto hrefs = nocal::sync::find_children(response, "D:href");
|
||||
require(hrefs.size() == 1 && hrefs[0]->text == "/calendars/user/calendar1/", "href");
|
||||
// D:displayname is inside D:propstat > D:prop, so traverse down
|
||||
const auto& propstat = response.children[1];
|
||||
require(propstat.tag == "D:propstat", "propstat element");
|
||||
const auto& prop = propstat.children[1];
|
||||
require(prop.tag == "D:prop", "prop element");
|
||||
const auto displaynames = nocal::sync::find_children(prop, "D:displayname");
|
||||
require(displaynames.size() == 1 && displaynames[0]->text == "My Calendar", "displayname");
|
||||
const auto sync_tokens = nocal::sync::find_children(prop, "D:sync-token");
|
||||
require(sync_tokens.size() == 1 && sync_tokens[0]->text == "sync-token-123", "sync-token");
|
||||
}
|
||||
|
||||
void test_max_size() {
|
||||
bool threw = false;
|
||||
try {
|
||||
const std::string large(5 * 1024 * 1024, 'x');
|
||||
(void)nocal::sync::parse_xml("<root>" + large + "</root>", 1024);
|
||||
} catch (const std::runtime_error&) {
|
||||
threw = true;
|
||||
}
|
||||
require(threw, "oversized document throws");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
try {
|
||||
test_simple_element();
|
||||
test_namespace_prefix();
|
||||
test_attributes();
|
||||
test_cdata();
|
||||
test_self_closing();
|
||||
test_nested_elements();
|
||||
test_entity_references();
|
||||
test_processing_instruction();
|
||||
test_comment_skipped();
|
||||
test_find_children();
|
||||
test_get_child_text();
|
||||
test_empty_document();
|
||||
test_unclosed_tag();
|
||||
test_mismatched_tag();
|
||||
test_max_depth();
|
||||
test_caldav_propfind_response();
|
||||
test_max_size();
|
||||
std::cout << "caldav_xml_tests: ok\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "caldav_xml_tests: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user