OXYGENHUB
◆ main openmsx / src / config / XMLElement.cc
16.0 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "XMLElement.hh"
2
3
#include "ConfigException.hh"
4
#include "File.hh"
5
#include "FileContext.hh" // for bw compat
6
#include "FileException.hh"
7
#include "StringOp.hh"
8
#include "XMLException.hh"
9
#include "XMLOutputStream.hh"
10
#include "rapidsax.hh"
11
#include "serialize.hh"
12
#include "serialize_meta.hh"
13
#include "serialize_stl.hh"
14
15
#include <algorithm>
16
#include <cassert>
17
#include <vector>
18
19
namespace openmsx {
20
21
// Returns nullptr when not found.
22
const XMLElement* XMLElement::findChild(std::string_view childName) const
23
{
24
	for (const auto* child = firstChild; child; child = child->nextSibling) {
25
		if (child->name == childName) {
26
			return child;
27
		}
28
	}
29
	return nullptr;
30
}
31
const XMLElement* XMLElement::findChildWithAttribute(std::string_view childName, std::string_view attrName, std::string_view attrValue) const
32
{
33
	for (const auto* child = firstChild; child; child = child->nextSibling) {
34
		if (child->name == childName && child->getAttributeValue(attrName, "") == attrValue) {
35
			return child;
36
		}
37
	}
38
	return nullptr;
39
}
40
XMLElement* XMLElement::findChild(std::string_view childName)
41
{
42
	for (auto* child = firstChild; child; child = child->nextSibling) {
43
		if (child->name == childName) {
44
			return child;
45
		}
46
	}
47
	return nullptr;
48
}
49
50
// Similar to above, but instead of starting the search from the start of the
51
// list, start searching at 'hint'. This 'hint' parameter must be initialized
52
// with 'firstChild', on each successful search this parameter is updated to
53
// point to the next child (so that the next search starts from there). If the
54
// end of the list is reached we restart the search from the start (so that the
55
// full list is searched before giving up).
56
XMLElement* XMLElement::findChild(std::string_view childName, XMLElement*& hint)
57
{
58
	for (auto* current = hint; current; current = current->nextSibling) {
59
		if (current->name == childName) {
60
			hint = current->nextSibling;
61
			return current;
62
		}
63
	}
64
	for (auto* current = firstChild; current != hint; current = current->nextSibling) {
65
		if (current->name == childName) {
66
			hint = current->nextSibling;
67
			return current;
68
		}
69
	}
70
	return nullptr;
71
}
72
73
// Like findChild(), but throws when not found.
74
const XMLElement& XMLElement::getChild(std::string_view childName) const
75
{
76
	if (const auto* elem = findChild(childName)) {
77
		return *elem;
78
	}
79
	throw ConfigException("Missing tag \"", childName, "\".");
80
}
81
82
XMLElement& XMLElement::getChild(std::string_view childName)
83
{
84
	if (auto* elem = findChild(childName)) {
85
		return *elem;
86
	}
87
	throw ConfigException("Missing tag \"", childName, "\".");
88
}
89
90
// Throws when not found.
91
std::string_view XMLElement::getChildData(std::string_view childName) const
92
{
93
	return getChild(childName).getData();
94
}
95
96
// Returns default value when not found.
97
std::string_view XMLElement::getChildData(
98
	std::string_view childName, std::string_view defaultValue) const
99
{
100
	const auto* child = findChild(childName);
101
	return child ? child->getData() : defaultValue;
102
}
103
104
bool XMLElement::getChildDataAsBool(std::string_view childName, bool defaultValue) const
105
{
106
	const auto* child = findChild(childName);
107
	return child ? StringOp::stringToBool(child->getData()) : defaultValue;
108
}
109
110
int XMLElement::getChildDataAsInt(std::string_view childName, int defaultValue) const
111
{
112
	const auto* child = findChild(childName);
113
	if (!child) return defaultValue;
114
	return StringOp::stringTo<int>(child->getData()).value_or(defaultValue);
115
}
116
117
size_t XMLElement::numChildren() const
118
{
119
	return std::ranges::distance(getChildren());
120
}
121
122
// Return nullptr when not found.
123
const XMLAttribute* XMLElement::findAttribute(std::string_view attrName) const
124
{
125
	for (const auto* attr = firstAttribute; attr; attr = attr->nextAttribute) {
126
		if (attr->getName() == attrName) {
127
			return attr;
128
		}
129
	}
130
	return nullptr;
131
}
132
XMLAttribute* XMLElement::findAttribute(std::string_view attrName)
133
{
134
	for (auto* attr = firstAttribute; attr; attr = attr->nextAttribute) {
135
		if (attr->getName() == attrName) {
136
			return attr;
137
		}
138
	}
139
	return nullptr;
140
}
141
142
// Throws when not found.
143
const XMLAttribute& XMLElement::getAttribute(std::string_view attrName) const
144
{
145
	if (const auto* result = findAttribute(attrName)) {
146
		return *result;
147
	}
148
	throw ConfigException("Missing attribute \"", attrName, "\".");
149
}
150
XMLAttribute& XMLElement::getAttribute(std::string_view attrName)
151
{
152
	if (auto* result = findAttribute(attrName)) {
153
		return *result;
154
	}
155
	throw ConfigException("Missing attribute \"", attrName, "\".");
156
}
157
158
// Throws when not found.
159
std::string_view XMLElement::getAttributeValue(std::string_view attrName) const
160
{
161
	return getAttribute(attrName).getValue();
162
}
163
164
std::string_view XMLElement::getAttributeValue(std::string_view attrName,
165
                                               std::string_view defaultValue) const
166
{
167
	const auto* attr = findAttribute(attrName);
168
	return attr ? attr->getValue() : defaultValue;
169
}
170
171
// Returns default value when not found.
172
bool XMLElement::getAttributeValueAsBool(std::string_view attrName,
173
                                         bool defaultValue) const
174
{
175
	const auto* attr = findAttribute(attrName);
176
	return attr ? StringOp::stringToBool(attr->getValue()) : defaultValue;
177
}
178
179
int XMLElement::getAttributeValueAsInt(std::string_view attrName,
180
                                       int defaultValue) const
181
{
182
	const auto* attr = findAttribute(attrName);
183
	if (!attr) return defaultValue;
184
	return StringOp::stringTo<int>(attr->getValue()).value_or(defaultValue);
185
}
186
187
// Like findAttribute(), but returns a pointer-to-the-XMLAttribute-pointer.
188
// This is mainly useful in combination with removeAttribute().
189
XMLAttribute** XMLElement::findAttributePointer(std::string_view attrName)
190
{
191
	for (auto** attr = &firstAttribute; *attr; attr = &(*attr)->nextAttribute) {
192
		if ((*attr)->getName() == attrName) {
193
			return attr;
194
		}
195
	}
196
	return nullptr;
197
}
198
199
// Remove a specific attribute from the (single-linked) list.
200
void XMLElement::removeAttribute(XMLAttribute** attrPtr)
201
{
202
	auto* attr = *attrPtr;
203
	*attrPtr = attr->nextAttribute;
204
}
205
206
size_t XMLElement::numAttributes() const
207
{
208
	return std::ranges::distance(getAttributes());
209
}
210
211
212
// Search for child with given name, if it doesn't exist yet, then create it now
213
// with given data. Don't change the data of an already existing child.
214
XMLElement* XMLDocument::getOrCreateChild(XMLElement& parent, const char* childName, const char* childData)
215
{
216
	auto** elem = &parent.firstChild;
217
	while (true) {
218
		if (!*elem) {
219
			auto* n = allocateElement(childName);
220
			n->setData(childData);
221
			*elem = n;
222
			return n;
223
		}
224
		if ((*elem)->getName() == childName) {
225
			return *elem;
226
		}
227
		elem = &(*elem)->nextSibling;
228
	}
229
}
230
231
// Search for child with given name, and change the data of that child. If the
232
// child didn't exist yet, then create it now (also with given data).
233
XMLElement* XMLDocument::setChildData(XMLElement& parent, const char* childName, const char* childData)
234
{
235
	auto** elem = &parent.firstChild;
236
	while (true) {
237
		if (!*elem) {
238
			auto* n = allocateElement(childName);
239
			*elem = n;
240
			(*elem)->setData(childData);
241
			return *elem;
242
		}
243
		if ((*elem)->getName() == childName) {
244
			(*elem)->setData(childData);
245
			return *elem;
246
		}
247
		elem = &(*elem)->nextSibling;
248
	}
249
}
250
251
// Set attribute to new value, if that attribute didn't exist yet, then also
252
// create it.
253
void XMLDocument::setAttribute(XMLElement& elem, const char* attrName, const char* attrValue)
254
{
255
	auto** attr = &elem.firstAttribute;
256
	while (true) {
257
		if (!*attr) {
258
			auto* a = allocateAttribute(attrName, attrValue);
259
			*attr = a;
260
			return;
261
		}
262
		if ((*attr)->getName() == attrName) {
263
			(*attr)->setValue(attrValue);
264
			return;
265
		}
266
		attr = &(*attr)->nextAttribute;
267
	}
268
}
269
270
271
// Helper to parse a XML file into a XMLDocument.
272
class XMLDocumentHandler : public rapidsax::NullHandler
273
{
274
public:
275
	explicit XMLDocumentHandler(XMLDocument& doc_)
276
		: doc(doc_)
277
		, nextElement(&doc.root) {}
278
279
	[[nodiscard]] std::string_view getSystemID() const { return systemID; }
280
281
	void start(zstring_view name) {
282
		stack.push_back(currentElement);
283
284
		auto* n = doc.allocateElement(name.c_str());
285
		currentElement = n;
286
287
		assert(*nextElement == nullptr);
288
		*nextElement = n;
289
		nextElement = &n->firstChild;
290
291
		nextAttribute = &n->firstAttribute;
292
	}
293
294
	void stop() {
295
		nextElement = &currentElement->nextSibling;
296
		nextAttribute = nullptr;
297
		currentElement = stack.back();
298
		stack.pop_back();
299
	}
300
301
	void text(zstring_view text) {
302
		currentElement->data = text.c_str();
303
	}
304
305
	void attribute(zstring_view name, zstring_view value) {
306
		auto* a = doc.allocateAttribute(name.c_str(), value.c_str());
307
308
		assert(nextAttribute);
309
		assert(*nextAttribute == nullptr);
310
		*nextAttribute = a;
311
		nextAttribute = &a->nextAttribute;
312
	}
313
314
	void doctype(zstring_view txt) {
315
		auto pos1 = txt.find(" SYSTEM ");
316
		if (pos1 == std::string_view::npos) return;
317
		if ((pos1 + 8) >= txt.size()) return;
318
		char q = txt[pos1 + 8];
319
		if (q != one_of('"', '\'')) return;
320
		auto t = txt.substr(pos1 + 9);
321
		auto pos2 = t.find(q);
322
		if (pos2 == std::string_view::npos) return;
323
324
		systemID = t.substr(0, pos2);
325
	}
326
327
private:
328
	XMLDocument& doc;
329
330
	std::string_view systemID;
331
	std::vector<XMLElement*> stack;
332
	XMLElement* currentElement = nullptr;
333
	XMLElement** nextElement = nullptr;
334
	XMLAttribute** nextAttribute = nullptr;
335
};
336
337
XMLElement* XMLDocument::allocateElement(const char* name)
338
{
339
	void* p = allocator.allocate(sizeof(XMLElement), alignof(XMLElement));
340
	return new (p) XMLElement(name);
341
}
342
343
XMLElement* XMLDocument::allocateElement(const char* name, const char* data)
344
{
345
	void* p = allocator.allocate(sizeof(XMLElement), alignof(XMLElement));
346
	return new (p) XMLElement(name, data);
347
}
348
349
XMLAttribute* XMLDocument::allocateAttribute(const char* name, const char* value)
350
{
351
	void* p = allocator.allocate(sizeof(XMLAttribute), alignof(XMLAttribute));
352
	return new (p) XMLAttribute(name, value);
353
}
354
355
const char* XMLDocument::allocateString(std::string_view str)
356
{
357
	auto* p = static_cast<char*>(allocator.allocate(str.size() + 1, alignof(char)));
358
	auto e = std::ranges::copy(str, p).out;
359
	*e = '\0';
360
	return p;
361
}
362
363
void XMLDocument::load(zstring_view filename, std::string_view systemID)
364
{
365
	assert(!root);
366
367
	try {
368
		buf = File(filename).mmap<char>(rapidsax::EXTRA_BUFFER_SPACE);
369
	} catch (FileException& e) {
370
		throw XMLException(filename, ": failed to read: ", e.getMessage());
371
	}
372
373
	XMLDocumentHandler handler(*this);
374
	try {
375
		rapidsax::parse<rapidsax::zeroTerminateStrings>(handler, buf.data());
376
	} catch (rapidsax::ParseError& e) {
377
		throw XMLException(filename, ": Document parsing failed: ", e.what());
378
	}
379
	if (!root) {
380
		throw XMLException(filename,
381
			": Document doesn't contain mandatory root Element");
382
	}
383
	if (handler.getSystemID().empty()) {
384
		throw XMLException(filename, ": Missing systemID.\n"
385
			"You're probably using an old incompatible file format.");
386
	}
387
	if (handler.getSystemID() != systemID) {
388
		throw XMLException(filename, ": systemID doesn't match "
389
			"(expected ", systemID, ", got ", handler.getSystemID(), ")\n"
390
			"You're probably using an old incompatible file format.");
391
	}
392
}
393
394
XMLElement* XMLDocument::loadElement(MemInputArchive& ar)
395
{
396
	auto name = ar.loadStr();
397
	if (name.empty()) return nullptr; // should only happen for empty document
398
	auto* elem = allocateElement(allocateString(name));
399
400
	unsigned numAttrs; ar.load(numAttrs);
401
	auto** attrPtr = &elem->firstAttribute;
402
	repeat(numAttrs, [&] {
403
		const char* n = allocateString(ar.loadStr());
404
		const char* v = allocateString(ar.loadStr());
405
		auto* attr = allocateAttribute(n, v);
406
		*attrPtr = attr;
407
		attrPtr = &attr->nextAttribute;
408
	});
409
410
	unsigned numElems; ar.load(numElems);
411
	if (numElems) {
412
		auto** elemPtr = &elem->firstChild;
413
		repeat(numElems, [&] {
414
			auto* n = loadElement(ar);
415
			*elemPtr = n;
416
			elemPtr = &n->nextSibling;
417
		});
418
	} else {
419
		auto data = ar.loadStr();
420
		if (!data.empty()) {
421
			elem->setData(allocateString(data));
422
		}
423
	}
424
425
	return elem;
426
}
427
428
void XMLDocument::serialize(MemInputArchive& ar, unsigned /*version*/)
429
{
430
	root = loadElement(ar);
431
}
432
433
static void saveElement(MemOutputArchive& ar, const XMLElement& elem)
434
{
435
	ar.save(elem.getName());
436
437
	ar.save(unsigned(elem.numAttributes()));
438
	for (const auto& attr : elem.getAttributes()) {
439
		ar.save(attr.getName());
440
		ar.save(attr.getValue());
441
	}
442
443
	auto numElems = unsigned(elem.numChildren());
444
	ar.save(numElems);
445
	if (numElems) {
446
		for (const auto& child : elem.getChildren()) {
447
			saveElement(ar, child);
448
		}
449
	} else {
450
		ar.save(elem.getData());
451
	}
452
}
453
454
void XMLDocument::serialize(MemOutputArchive& ar, unsigned /*version*/) const
455
{
456
	if (root) {
457
		saveElement(ar, *root);
458
	} else {
459
		std::string_view empty;
460
		ar.save(empty);
461
	}
462
}
463
464
XMLElement* XMLDocument::clone(const XMLElement& inElem)
465
{
466
	auto* outElem = allocateElement(allocateString(inElem.getName()));
467
468
	auto** attrPtr = &outElem->firstAttribute;
469
	for (const auto& inAttr : inElem.getAttributes()) {
470
		const char* n = allocateString(inAttr.getName());
471
		const char* v = allocateString(inAttr.getValue());
472
		auto* outAttr = allocateAttribute(n, v);
473
		*attrPtr = outAttr;
474
		attrPtr = &outAttr->nextAttribute;
475
	}
476
477
	if (auto data = inElem.getData() ; !data.empty()) {
478
		outElem->setData(allocateString(data));
479
	}
480
481
	auto** childPtr = &outElem->firstChild;
482
	for (const auto& inChild : inElem.getChildren()) {
483
		auto* outChild = clone(inChild);
484
		*childPtr = outChild;
485
		childPtr = &outChild->nextSibling;
486
	}
487
488
	return outElem;
489
}
490
491
void XMLDocument::serialize(XmlInputArchive& ar, unsigned /*version*/)
492
{
493
	const auto* current = ar.currentElement();
494
	if (const auto* elem = current->getFirstChild()) {
495
		assert(elem->nextSibling == nullptr); // at most 1 child
496
		root = clone(*elem);
497
	}
498
}
499
500
static void saveElement(XMLOutputStream<XmlOutputArchive>& stream, const XMLElement& elem)
501
{
502
	stream.with_tag(elem.getName(), [&]{
503
		for (const auto& attr : elem.getAttributes()) {
504
			stream.attribute(attr.getName(), attr.getValue());
505
		}
506
		if (elem.hasChildren()) {
507
			for (const auto& child : elem.getChildren()) {
508
				saveElement(stream, child);
509
			}
510
		} else {
511
			stream.data(elem.getData());
512
		}
513
	});
514
}
515
516
void XMLDocument::serialize(XmlOutputArchive& ar, unsigned /*version*/) const
517
{
518
	auto& stream = ar.getXMLOutputStream();
519
	if (root) {
520
		saveElement(stream, *root);
521
	}
522
}
523
524
XMLElement* XMLDocument::clone(const OldXMLElement& inElem)
525
{
526
	auto* outElem = allocateElement(allocateString(inElem.name));
527
528
	auto** attrPtr = &outElem->firstAttribute;
529
	for (const auto& [inName, inValue] : inElem.attributes) {
530
		const char* n = allocateString(inName);
531
		const char* v = allocateString(inValue);
532
		auto* outAttr = allocateAttribute(n, v);
533
		*attrPtr = outAttr;
534
		attrPtr = &outAttr->nextAttribute;
535
	}
536
537
	if (!inElem.data.empty()) {
538
		outElem->setData(allocateString(inElem.data));
539
	}
540
541
	auto** childPtr = &outElem->firstChild;
542
	for (const auto& inChild : inElem.children) {
543
		auto* outChild = clone(inChild);
544
		*childPtr = outChild;
545
		childPtr = &outChild->nextSibling;
546
	}
547
548
	return outElem;
549
}
550
551
void XMLDocument::load(const OldXMLElement& elem)
552
{
553
	root = clone(elem);
554
}
555
556
557
static std::unique_ptr<FileContext> lastSerializedFileContext;
558
std::unique_ptr<FileContext> OldXMLElement::getLastSerializedFileContext()
559
{
560
	return std::move(lastSerializedFileContext);
561
}
562
// version 1: initial version
563
// version 2: removed 'context' tag
564
//            also removed 'parent', but that was never serialized
565
//        2b: (no need to increase version) name and data members are
566
//            serialized as normal members instead of constructor parameters
567
//        2c: (no need to increase version) attributes were initially stored as
568
//            map<string, string>, later this was changed to
569
//            vector<pair<string, string>>. To keep bw-compat the serialize()
570
//            method converted between these two formats. Though (by luck) in
571
//            the XML output both data structures are serialized to the same
572
//            format, so we can drop this conversion step without breaking
573
//            bw-compat.
574
template<typename Archive>
575
void OldXMLElement::serialize(Archive& ar, unsigned version)
576
{
577
	assert(Archive::IS_LOADER);
578
	ar.serialize("name",       name,
579
	             "data",       data,
580
	             "attributes", attributes,
581
	             "children",   children);
582
583
	if (ar.versionBelow(version, 2)) {
584
		std::unique_ptr<FileContext> context;
585
		ar.serialize("context", context);
586
		if (context) {
587
			lastSerializedFileContext = std::move(context);
588
		}
589
	}
590
}
591
INSTANTIATE_SERIALIZE_METHODS(OldXMLElement);
592
593
} // namespace openmsx