OXYGENHUB
◆ main openmsx / src / serialize.cc
13.7 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "serialize.hh"
2
3
#include "FileOperations.hh"
4
#include "Version.hh"
5
#include "XMLElement.hh"
6
#include "XMLException.hh"
7
8
#include "Base64.hh"
9
#include "Date.hh"
10
#include "DeltaBlock.hh"
11
#include "HexDump.hh"
12
#include "MemBuffer.hh"
13
#include "narrow.hh"
14
#include "one_of.hh"
15
#include "stl.hh"
16
17
#include "build-info.hh"
18
19
#include "cstdiop.hh" // for dup()
20
#include <bit>
21
#include <cstdint>
22
#include <cstring>
23
#include <iostream>
24
#include <limits>
25
26
namespace openmsx {
27
28
using namespace std::literals;
29
30
template<typename Derived>
31
void ArchiveBase<Derived>::attribute(const char* name, const char* value)
32
{
33
	std::string valueStr(value);
34
	self().attribute(name, valueStr);
35
}
36
template class ArchiveBase<MemOutputArchive>;
37
template class ArchiveBase<XmlOutputArchive>;
38
39
////
40
41
unsigned OutputArchiveBase2::generateID1(const void* p)
42
{
43
	#ifdef linux
44
	assert("Can't serialize ID of object located on the stack" &&
45
	       !addressOnStack(p));
46
	#endif
47
	++lastId;
48
	assert(!polyIdMap.contains(p));
49
	polyIdMap.emplace_noDuplicateCheck(p, lastId);
50
	return lastId;
51
}
52
unsigned OutputArchiveBase2::generateID2(
53
	const void* p, const std::type_info& typeInfo)
54
{
55
	#ifdef linux
56
	assert("Can't serialize ID of object located on the stack" &&
57
	       !addressOnStack(p));
58
	#endif
59
	++lastId;
60
	auto key = std::pair(p, std::type_index(typeInfo));
61
	assert(!idMap.contains(key));
62
	idMap.emplace_noDuplicateCheck(key, lastId);
63
	return lastId;
64
}
65
66
unsigned OutputArchiveBase2::getID1(const void* p)
67
{
68
	const auto* v = lookup(polyIdMap, p);
69
	return v ? *v : 0;
70
}
71
unsigned OutputArchiveBase2::getID2(
72
	const void* p, const std::type_info& typeInfo)
73
{
74
	const auto* v = lookup(idMap, std::pair(p, std::type_index(typeInfo)));
75
	return v ? *v : 0;
76
}
77
78
79
template class OutputArchiveBase<MemOutputArchive>;
80
template class OutputArchiveBase<XmlOutputArchive>;
81
82
////
83
84
void* InputArchiveBase2::getPointer(unsigned id)
85
{
86
	auto* v = lookup(idMap, id);
87
	return v ? *v : nullptr;
88
}
89
90
void InputArchiveBase2::addPointer(unsigned id, const void* p)
91
{
92
	assert(!idMap.contains(id));
93
	idMap.emplace_noDuplicateCheck(id, const_cast<void*>(p));
94
}
95
96
unsigned InputArchiveBase2::getId(const void* ptr) const
97
{
98
	for (const auto& [id, pt] : idMap) {
99
		if (pt == ptr) return id;
100
	}
101
	return 0;
102
}
103
104
template class InputArchiveBase<MemInputArchive>;
105
template class InputArchiveBase<XmlInputArchive>;
106
107
////
108
109
void MemOutputArchive::save(std::string_view s)
110
{
111
	auto size = s.size();
112
	auto buf = buffer.allocate(sizeof(size) + size);
113
	memcpy(buf.data(), &size, sizeof(size));
114
	copy_to_range(s, subspan(buf, sizeof(size)));
115
}
116
117
////
118
119
void MemInputArchive::load(std::string& s)
120
{
121
	size_t length;
122
	load(length);
123
	s.resize_and_overwrite(length, [&](char* dst, size_t /*n*/) {
124
		//assert(length == n); <-- not true with gcc-12 (bug)
125
		if (length) {
126
			buffer.read(dst, length);
127
		}
128
		return length;
129
	});
130
}
131
132
std::string_view MemInputArchive::loadStr()
133
{
134
	size_t length;
135
	load(length);
136
	const uint8_t* p = buffer.getCurrentPos();
137
	buffer.skip(length);
138
	return {std::bit_cast<const char*>(p), length};
139
}
140
141
////
142
143
// Too small inputs don't compress very well (often the compressed size is even
144
// bigger than the input). It also takes a relatively long time (because often
145
// compression has a relatively large setup time). I choose this value
146
// semi-arbitrary. I only made it >= 52 so that the (incompressible) RP5C01
147
// registers won't be compressed.
148
static constexpr size_t SMALL_SIZE = 64;
149
void MemOutputArchive::serialize_blob(const char* /*tag*/, std::span<const uint8_t> data,
150
                                      bool diff)
151
{
152
	// Delta-compress in-memory blobs, see DeltaBlock.hh for more details.
153
	if (data.size() > SMALL_SIZE) {
154
		auto deltaBlockIdx = unsigned(deltaBlocks.size());
155
		save(deltaBlockIdx); // see comment below in MemInputArchive
156
		deltaBlocks.push_back(diff
157
			? lastDeltaBlocks.createNew(data.data(), data)
158
			: lastDeltaBlocks.createNullDiff(data.data(), data));
159
	} else {
160
		auto buf = buffer.allocate(data.size());
161
		copy_to_range(data, buf);
162
	}
163
}
164
165
void MemInputArchive::serialize_blob(const char* /*tag*/, std::span<uint8_t> data,
166
                                     bool /*diff*/)
167
{
168
	if (data.size() > SMALL_SIZE) {
169
		// Usually blobs are saved in the same order as they are loaded
170
		// (via the serialize_blob() methods in respectively
171
		// MemOutputArchive and MemInputArchive). In that case keeping
172
		// track of the deltaBlockIdx in the savestate itself is
173
		// redundant (it will simply be an increasing value). However
174
		// in rare cases, via the {begin,end,skip)Section() methods, it
175
		// is possible that certain blobs are stored in the savestate,
176
		// but skipped while loading. That's why we do need the index.
177
		unsigned deltaBlockIdx; load(deltaBlockIdx);
178
		deltaBlocks[deltaBlockIdx]->apply(data);
179
	} else {
180
		copy_to_range(std::span{buffer.getCurrentPos(), data.size()}, data);
181
		buffer.skip(data.size());
182
	}
183
}
184
185
////
186
187
XmlOutputArchive::XmlOutputArchive(zstring_view filename_)
188
	: filename(filename_)
189
	, writer(*this)
190
{
191
	{
192
		auto f = FileOperations::openFile(filename, "wb");
193
		if (!f) error();
194
		int duped_fd = dup(fileno(f.get()));
195
		if (duped_fd == -1) error();
196
		file = gzdopen(duped_fd, "wb9");
197
		if (!file) {
198
			::close(duped_fd);
199
			error();
200
		}
201
		// on scope-exit 'f' is closed, and 'file'
202
		// uses the dup()'ed file descriptor.
203
	}
204
205
	static constexpr std::string_view header =
206
		"<?xml version=\"1.0\" ?>\n"
207
		"<!DOCTYPE openmsx-serialize SYSTEM 'openmsx-serialize.dtd'>\n";
208
	write(header);
209
210
	writer.begin("serial");
211
	writer.attribute("openmsx_version", Version::full());
212
	writer.attribute("date_time", Date::toString(time(nullptr)));
213
	writer.attribute("platform", TARGET_PLATFORM);
214
}
215
216
void XmlOutputArchive::close()
217
{
218
	if (!file) return; // already closed
219
220
	writer.end("serial");
221
222
	if (gzclose(file) != Z_OK) {
223
		error();
224
	}
225
	file = nullptr;
226
}
227
228
XmlOutputArchive::~XmlOutputArchive()
229
{
230
	try {
231
		close();
232
	} catch (...) {
233
		// Eat exception. Explicitly call close() if you want to handle errors.
234
	}
235
}
236
237
void XmlOutputArchive::write(std::span<const char> buf)
238
{
239
	if ((gzwrite(file, buf.data(), unsigned(buf.size())) == 0) && !buf.empty()) {
240
		error();
241
	}
242
}
243
244
void XmlOutputArchive::write1(char c)
245
{
246
	if (gzputc(file, c) == -1) {
247
		error();
248
	}
249
}
250
251
void XmlOutputArchive::check(bool condition) const
252
{
253
	assert(condition); (void)condition;
254
}
255
256
void XmlOutputArchive::error()
257
{
258
	if (file) {
259
		gzclose(file);
260
		file = nullptr;
261
	}
262
	throw XMLException("could not write \"", filename, '"');
263
}
264
265
void XmlOutputArchive::saveChar(char c)
266
{
267
	writer.data(std::string_view(&c, 1));
268
}
269
void XmlOutputArchive::save(std::string_view str)
270
{
271
	writer.data(str);
272
}
273
void XmlOutputArchive::save(bool b)
274
{
275
	writer.data(b ? "true"sv : "false"sv);
276
}
277
void XmlOutputArchive::save(unsigned char b)
278
{
279
	save(unsigned(b));
280
}
281
void XmlOutputArchive::save(signed char c)
282
{
283
	save(int(c));
284
}
285
void XmlOutputArchive::save(char c)
286
{
287
	save(int(c));
288
}
289
void XmlOutputArchive::save(int i)
290
{
291
	saveImpl(i);
292
}
293
void XmlOutputArchive::save(unsigned u)
294
{
295
	saveImpl(u);
296
}
297
void XmlOutputArchive::save(unsigned long long ull)
298
{
299
	saveImpl(ull);
300
}
301
302
void XmlOutputArchive::attribute(const char* name, std::string_view str)
303
{
304
	writer.attribute(name, str);
305
}
306
void XmlOutputArchive::attribute(const char* name, int i)
307
{
308
	attributeImpl(name, i);
309
}
310
void XmlOutputArchive::attribute(const char* name, unsigned u)
311
{
312
	attributeImpl(name, u);
313
}
314
315
void XmlOutputArchive::beginTag(const char* tag)
316
{
317
	writer.begin(tag);
318
}
319
void XmlOutputArchive::endTag(const char* tag)
320
{
321
	writer.end(tag);
322
}
323
324
void XmlOutputArchive::serialize_blob(
325
	const char* tag, std::span<const uint8_t> data, bool /*diff*/)
326
{
327
	std::string_view encoding;
328
	std::string tmp;
329
	if (false) {
330
		// useful for debugging
331
		encoding = "hex";
332
		tmp = HexDump::encode(data);
333
	} else if (false) {
334
		encoding = "base64";
335
		tmp = Base64::encode(data);
336
	} else {
337
		encoding = "gz-base64";
338
		// TODO check for overflow?
339
		auto len = data.size();
340
		auto dstLen = uLongf(len + len / 1000 + 12 + 1); // worst-case
341
		MemBuffer<uint8_t> buf(dstLen);
342
		if (compress2(buf.data(), &dstLen,
343
		              std::bit_cast<const Bytef*>(data.data()),
344
		              uLong(len), 9)
345
		    != Z_OK) {
346
			throw MSXException("Error while compressing blob.");
347
		}
348
		tmp = Base64::encode(buf.first(dstLen));
349
	}
350
	writer.begin(tag);
351
	writer.attribute("encoding", encoding);
352
	writer.dataRaw(tmp);
353
	writer.end(tag);
354
}
355
356
////
357
358
XmlInputArchive::XmlInputArchive(zstring_view filename)
359
{
360
	xmlDoc.load(filename, "openmsx-serialize.dtd");
361
	auto* root = xmlDoc.getRoot();
362
	elems.emplace_back(root, root->getFirstChild());
363
}
364
365
std::string_view XmlInputArchive::loadStr() const
366
{
367
	if (currentElement()->hasChildren()) {
368
		throw XMLException("No child tags expected for primitive type");
369
	}
370
	return currentElement()->getData();
371
}
372
void XmlInputArchive::load(std::string& t) const
373
{
374
	t = loadStr();
375
}
376
void XmlInputArchive::loadChar(char& c) const
377
{
378
	std::string str;
379
	load(str);
380
	std::istringstream is(str);
381
	is >> c;
382
}
383
void XmlInputArchive::load(bool& b) const
384
{
385
	std::string_view s = loadStr();
386
	if (s == one_of("true", "1")) {
387
		b = true;
388
	} else if (s == one_of("false", "0")) {
389
		b = false;
390
	} else {
391
		throw XMLException("Bad value found for boolean: ", s);
392
	}
393
}
394
395
// This function parses a number from a string. It's similar to the generic
396
// templatized XmlInputArchive::load() method, but _much_ faster. It does
397
// have some limitations though:
398
//  - it can't handle leading whitespace
399
//  - it can't handle extra characters at the end of the string
400
//  - it can only handle one base (only decimal, not octal or hexadecimal)
401
//  - it doesn't understand a leading '+' sign
402
//  - it doesn't detect overflow or underflow (The generic implementation sets
403
//    a 'bad' flag on the stream and clips the result to the min/max allowed
404
//    value. Though this 'bad' flag was ignored by the openMSX code).
405
// This routine is only used to parse strings we've written ourselves (and the
406
// savestate/replay XML files are not meant to be manually edited). So the
407
// above limitations don't really matter. And we can use the speed gain.
408
template<std::integral T> static inline void fastAtoi(std::string_view str, T& t)
409
{
410
	t = 0;
411
	bool neg = false;
412
	size_t i = 0;
413
	size_t l = str.size();
414
415
	if constexpr (std::numeric_limits<T>::is_signed) {
416
		if (l == 0) return;
417
		if (str[0] == '-') {
418
			neg = true;
419
			i = 1;
420
		}
421
	}
422
	for (/**/; i < l; ++i) {
423
		unsigned d = str[i] - '0';
424
		if (d > 9) [[unlikely]] {
425
			throw XMLException("Invalid integer: ", str);
426
		}
427
		t = 10 * t + d;
428
	}
429
	if constexpr (std::numeric_limits<T>::is_signed) {
430
		if (neg) t = -t;
431
	} else {
432
		assert(!neg); (void)neg;
433
	}
434
}
435
void XmlInputArchive::load(int& i) const
436
{
437
	std::string_view str = loadStr();
438
	fastAtoi(str, i);
439
}
440
void XmlInputArchive::load(unsigned& u) const
441
{
442
	std::string_view str = loadStr();
443
	try {
444
		fastAtoi(str, u);
445
	} catch (XMLException&) {
446
		// One reason could be that the type of a member was corrected
447
		// from 'int' to 'unsigned'. In that case loading an old
448
		// savestate (that contains a negative value) might fail. So try
449
		// again parsing as an 'int'.
450
		int i;
451
		fastAtoi(str, i);
452
		u = narrow_cast<unsigned>(i);
453
	}
454
}
455
void XmlInputArchive::load(unsigned long long& ull) const
456
{
457
	std::string_view str = loadStr();
458
	fastAtoi(str, ull);
459
}
460
void XmlInputArchive::load(unsigned char& b) const
461
{
462
	unsigned u;
463
	load(u);
464
	b = narrow_cast<unsigned char>(u);
465
}
466
void XmlInputArchive::load(signed char& c) const
467
{
468
	int i;
469
	load(i);
470
	c = narrow_cast<signed char>(i);
471
}
472
void XmlInputArchive::load(char& c) const
473
{
474
	int i;
475
	load(i);
476
	c = narrow_cast<char>(i);
477
}
478
479
void XmlInputArchive::beginTag(const char* tag)
480
{
481
	auto* child = currentElement()->findChild(tag, elems.back().second);
482
	if (!child) {
483
		std::string path;
484
		for (const auto& [e, _] : elems) {
485
			strAppend(path, e->getName(), '/');
486
		}
487
		throw XMLException("No child tag \"", tag,
488
		                   "\" found at location \"", path, '\"');
489
	}
490
	elems.emplace_back(child, child->getFirstChild());
491
}
492
void XmlInputArchive::endTag(const char* tag)
493
{
494
	auto& elem = *currentElement();
495
	if (elem.getName() != tag) {
496
		throw XMLException("End tag \"", elem.getName(),
497
		                   "\" not equal to begin tag \"", tag, "\"");
498
	}
499
	elem.clearName(); // mark this elem for later beginTag() calls
500
	elems.pop_back();
501
}
502
503
void XmlInputArchive::attribute(const char* name, std::string& t) const
504
{
505
	const auto* attr = currentElement()->findAttribute(name);
506
	if (!attr) {
507
		throw XMLException("Missing attribute \"", name, "\".");
508
	}
509
	t = attr->getValue();
510
}
511
void XmlInputArchive::attribute(const char* name, int& i) const
512
{
513
	attributeImpl(name, i);
514
}
515
void XmlInputArchive::attribute(const char* name, unsigned& u) const
516
{
517
	attributeImpl(name, u);
518
}
519
bool XmlInputArchive::hasAttribute(const char* name) const
520
{
521
	return currentElement()->findAttribute(name);
522
}
523
int XmlInputArchive::countChildren() const
524
{
525
	return int(currentElement()->numChildren());
526
}
527
528
void XmlInputArchive::serialize_blob(
529
	const char* tag, std::span<uint8_t> data, bool /*diff*/)
530
{
531
	this->self().beginTag(tag);
532
	std::string encoding;
533
	this->self().attribute("encoding", encoding);
534
535
	std::string_view tmp = this->self().loadStr();
536
	this->self().endTag(tag);
537
538
	if (encoding == "gz-base64") {
539
		auto buf = Base64::decode(tmp);
540
		auto dstLen = uLongf(data.size()); // TODO check for overflow?
541
		if ((uncompress(std::bit_cast<Bytef*>(data.data()), &dstLen,
542
		                std::bit_cast<const Bytef*>(buf.data()), uLong(buf.size()))
543
		     != Z_OK) ||
544
		    (dstLen != data.size())) {
545
			throw MSXException("Error while decompressing blob.");
546
		}
547
	} else if (encoding == one_of("hex", "base64")) {
548
		bool ok = (encoding == "hex")
549
		        ? HexDump::decode_inplace(tmp, data)
550
		        : Base64 ::decode_inplace(tmp, data);
551
		if (!ok) {
552
			throw XMLException(
553
				"Length of decoded blob different from "
554
				"expected value (", data.size(), ')');
555
		}
556
	} else {
557
		throw XMLException("Unsupported encoding \"", encoding, "\" for blob");
558
	}
559
}
560
561
} // namespace openmsx