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.
207 lines
6.8 KiB
C++
207 lines
6.8 KiB
C++
#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;
|
|
}
|
|
}
|